diff --git a/CODEOWNERS b/CODEOWNERS index 6f2e6f3..a5af94d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,24 +1,5 @@ -# This CODEOWNERS file denotes the project leads -# and encodes their responsibilities for code review. - -# Instructions: At a minimum, replace the '@GITHUB_USER_NAME_GOES_HERE' -# here with at least one project lead. - -# Lines starting with '#' are comments. -# Each line is a file pattern followed by one or more owners. -# The format is described: https://github.blog/2017-07-06-introducing-code-owners/ - -# These owners will be the default owners for everything in the repo. -* @daveh-beep - - -# ----------------------------------------------- -# BELOW THIS LINE ARE TEMPLATES, UNUSED -# ----------------------------------------------- -# Order is important. The last matching pattern has the most precedence. -# So if a pull request only touches javascript files, only these owners -# will be requested to review. -# *.js @octocat @github/js - -# You can also use email addresses if you prefer. -# docs/* docs@example.com \ No newline at end of file +# Upstream Chalkline review routing; not an adopting team's language ownership. +# Adopters: review docs/adoption-checklist.md and replace this owner explicitly. +# Normative protocol/frontmatter changes also require both maintainer roles +# described in GOVERNANCE.md; this default route does not enforce that rule. +* @daveh-beep diff --git a/README.md b/README.md index 3576ef0..22b27d2 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ Chalkline helps a team turn its approved voice, terminology, vocabulary, and cha This open-source project shares reusable methods for maintaining language guidance, not a company's private style guide. It ships fictional examples, a setup protocol, and optional local tools. Your team supplies its own rules and decides who can approve them. +## Status + +Experimental protocol and template. Local structural checks and synthetic regressions run in CI; they do not verify writing quality or model adherence. Cross-harness setup parity, real-team setup burden, and ingest limits still need field validation. Cross-repository snapshot projection is a manual review contract, not bundled enforcement. See [sharing readiness and the next experiments](docs/sharing-readiness.md). + ## The problem Your agents write now — support replies, product copy, lifecycle email, release notes. What they know about *your* voice is whatever happened to be in the prompt — and every new prompt, agent file, and skill restates that voice from memory, drifting a little each time. Style guides live in PDFs and wikis that no tool consults. Teams that skip setup blame the tools for output that was never given the chance to be right. @@ -48,7 +52,7 @@ Two to four small files plus an optional calibration pair, not a hundred. You co Target time: under 30 minutes. We're testing that target through real setup experiences. -The upstream `GOVERNANCE.md`, `CODEOWNERS`, `CONTRIBUTING.md`, and `.github/` settings govern contributions to Chalkline itself. When adopting the template, review or replace those settings for your own repository; preserve applicable license and copyright notices. Using Chalkline does not place your language decisions under Block governance or appoint Chalkline maintainers as your reviewers. Agents should propose these changes, never assign owners or remove notices silently. +The upstream `GOVERNANCE.md`, `CODEOWNERS`, `CONTRIBUTING.md`, and `.github/` settings govern contributions to Chalkline itself. When adopting the template, use the [adoption checklist](docs/adoption-checklist.md) to review or replace those settings for your own repository; preserve applicable license and copyright notices. Using Chalkline does not place your language decisions under Block governance or appoint Chalkline maintainers as your reviewers. Agents should propose these changes, never assign owners or remove notices silently. Before pasting style guides, customer copy, or other source material, read [Safety and privacy](SAFETY.md). Git history can preserve content after you delete it. diff --git a/docs/adoption-checklist.md b/docs/adoption-checklist.md new file mode 100644 index 0000000..315c122 --- /dev/null +++ b/docs/adoption-checklist.md @@ -0,0 +1,16 @@ +# Adopting the template + +Complete this review before committing your team's setup. These are repository settings, not new interview questions or language rules. Do not assign owners, remove notices, or publish automatically. + +- [ ] Confirm repository visibility and the agent/model provider's data practices. Use only authorized, repository-safe material; see [Safety](../SAFETY.md). +- [ ] Replace or deliberately retain `CODEOWNERS`. Its current owner routes upstream Chalkline reviews, not your team's language decisions. +- [ ] Review `GOVERNANCE.md` and `CONTRIBUTING.md`. Replace upstream processes and links with your own approved ownership and contribution decisions. +- [ ] Review `.github/` issue forms, PR template, workflows, permissions, and environments. In particular, disable or replace the Pages workflow unless you deliberately intend to publish its static site. Check upstream issue labels and contact links. +- [ ] Remove or adapt `website/` if you do not need the upstream explainer. Review upstream GitHub links before using it as your own site. +- [ ] Preserve applicable license and copyright notices. Do not assume a template adoption changes their requirements. +- [ ] Review the generated reference diff, provenance, exact-wording scope, and any separately approved calibration pair. Record unresolved ownership decisions instead of inventing an approver. +- [ ] Decide separately whether to approve references, commit changes, and publish or push the repository. + +For use in another repository, follow [PINNING.md](../PINNING.md). A source pin or search index is not a validated snapshot; drafting agents must not fetch upstream instruction files. + +This checklist does not verify authority, provider privacy, repository permissions, or that an agent followed the protocol. The adopting owner reviews those decisions. diff --git a/docs/optional-semantic-checks.md b/docs/optional-semantic-checks.md new file mode 100644 index 0000000..d8619f0 --- /dev/null +++ b/docs/optional-semantic-checks.md @@ -0,0 +1,28 @@ +# Optional semantic-check experiment + +No TypeSafe/Jev adapter is bundled. This is an experiment plan, not a supported API or an effectiveness claim. Markdown remains canonical; setup, structural checks, and receipt arithmetic need no network or API key. + +## Start with fact preservation + +Use synthetic supplied facts and candidate drafts. Code extracts bounded candidate spans such as amounts, dates, identifiers, negations, and modal words. An optional semantic model evaluates narrow questions about each fact, such as whether a stated possibility became a promise or a failed request became a confirmed reservation. A generative model still writes copy; the semantic model does not rewrite it. + +For retrieval in a larger corpus, first retrieve and filter candidates in code, then evaluate semantic relevance against the supplied task, audience, channel, and locale. Keep no-coverage, unavailable-source, invalid-source, and conflict outcomes distinct. Ranking does not approve a rule or resolve precedence. + +## Keep code and people in control + +- Code owns parsing, exact comparisons, hashes, arithmetic, explicit scope/precedence, and result routing. Never use a model to sanitize upstream instruction repositories. +- People own authority and all four approval decisions. A semantic answer cannot create a human decision or promote output into guidance. +- Ask independent questions over the same small state together. Do not send the entire repository when only a fact and its surrounding draft are needed. +- Keep raw judgments separate from policy thresholds. TypeSafe Choice/Score confidence and Noul probability are not Chalkline's ordinal 0–3 evidence assessment. +- Expose uncertain answers, errors, timeouts, and skipped checks. Never display an unavailable semantic check as a pass. Requiring a check is an explicit adopting-owner decision. +- Keep credentials server-side or in the local caller's authorized environment. Review provider data practices; avoid body-level debug logging of private inputs. The static site stays offline. + +## Acceptance experiment + +Predeclare a synthetic suite containing preserved and changed facts, ambiguous dates, unsupported causes, added retry instructions, and irrelevant retrieved passages. Have a separate reviewer label cases without seeing model results. Split tuning and holdout cases; run a deterministic-only control alongside the optional adapter. + +Report false negatives and false positives, uncertainty/escalation rate, service failures, cost, end-to-end latency, and review effort. Report the exact model, SDK, questions, candidate spans, and thresholds. Choose thresholds from the consequences and held-out evidence, not a cookbook number. Include adversarially framed source data and option-order checks; typed output does not guarantee truth or injection resistance. + +Proceed only if the adapter catches the targeted mutations without unacceptable false alarms or burden on the holdout. It must not change supplied factual strength, suppress blockers, invent evidence, or approve publication. Keep the default tools and CI independent of SDK installation and paid calls. + +Before implementation, read the current [TypeSafe docs](https://docs.typesafe.ai/llms.txt), [confidence guidance](https://docs.typesafe.ai/confidence), and [model limitations](https://docs.typesafe.ai/model-jaggedness/jev-1.13). Recheck the applicable model's limitations; Jev 1.13 documents injection susceptibility, context distraction, and option-order effects. diff --git a/docs/sharing-readiness.md b/docs/sharing-readiness.md new file mode 100644 index 0000000..56b07be --- /dev/null +++ b/docs/sharing-readiness.md @@ -0,0 +1,31 @@ +# Sharing readiness + +Chalkline is an experimental protocol and template for team-owned language guidance. Share it as a method to try, not a verified enforcement or compliance system. + +## What has evidence + +- Python regression tests cover the local corpus parser, filesystem restrictions, output behavior, and receipt structure/arithmetic. +- CI runs contract-text assertions and validates fictional corpora. Text assertions do not execute agent behavior. +- Website tests exercise fixed fictional tasks and blocked/restored states. The site has no live model, uploads, or telemetry. +- [The July red-team receipt](../checks/RED-TEAM-2026-07-30.md) records single-run observations in three harnesses. It is historical evidence under those conditions, not current setup parity or a reliability estimate. + +## What remains unverified + +| Work | Current boundary | Next evidence | +|---|---|---| +| Cross-harness setup | Portable instructions, not demonstrated current parity | Run the same synthetic interview in goose, Claude Code, and Codex; [issue #3](https://github.com/block/chalkline/issues/3) | +| Real-team adoption | The under-30-minute setup target is still a target | Recruit a consenting pilot; record task-level elapsed time, stalls, approval burden, and safe gaps; [issue #6](https://github.com/block/chalkline/issues/6) | +| Ingest quality | At most 10 candidate rules per artifact; no measured semantic input budget | Compare predeclared small/large synthetic inputs and unchanged controls; [issue #5](https://github.com/block/chalkline/issues/5) | +| Cross-repository consumption | Manual review and local snapshot contract; no bundled validator/projector | An independently reviewed projector and consuming-harness conformance tests before claiming automated enforcement | +| Website accessibility | DOM-stub tests and historical visual checks | Keyboard, mobile, and screen-reader review; no full accessibility audit is claimed | +| Semantic checking | No Jev integration or automatic fact-preservation check | Evaluate an optional adapter using the [semantic-check experiment](optional-semantic-checks.md) | + +## Next execution plan + +1. Ship sharing cleanup: current site claims, an adoption checklist, visible structure-only receipt validation, and bounded local corpus reads with synthetic regressions. +2. Run the synthetic setup across the actual target harnesses. Predeclare cases and repetitions, separate reviewer material, preserve unchanged controls, and report failures and unknown settings. Follow [the behavioral-fixture procedure](../checks/behavioral/README.md); do not treat existing unit tests as this evidence. +3. Run a small consenting-team pilot. Keep private artifacts and raw agent sessions out of this public repository. Publish only manually reviewed, sanitized findings or synthetic reproductions. +4. Evaluate optional semantic checks only after the pilot identifies a recurring problem. Keep ordinary setup and local tools dependency-free. +5. Build snapshot projection only if cross-repository use becomes a demonstrated need. Explicit file allowlists and code checks help, but cannot prove arbitrary prose harmless. Keep consuming-owner review and harness access controls. + +No change in steps 2–5 is approved by this plan. Language approval, artifact approval, reusable calibration approval, and consumer deployment remain separate decisions. Upstream normative changes require the two maintainer roles in [Governance](../GOVERNANCE.md). diff --git a/tests/test_corpus.py b/tests/test_corpus.py index df4a10c..8c046e9 100644 --- a/tests/test_corpus.py +++ b/tests/test_corpus.py @@ -2,6 +2,7 @@ import hashlib import importlib.util +import io import json import os from pathlib import Path @@ -9,6 +10,7 @@ import sys import tempfile import unittest +from unittest import mock TOOL = Path(__file__).resolve().parents[1] / "tools" / "corpus.py" # Avoid importlib's bytecode cache: the tool and tests need no disk caches. @@ -69,6 +71,98 @@ def test_nested_hashes_and_determinism(self): first.write_bytes(first.read_bytes() + b"\n") self.assertNotEqual(before.stdout, self.cli("index").stdout) + def assert_limit_failure(self, constant, limit, message): + with mock.patch.object(corpus, constant, limit): + with self.assertRaisesRegex(corpus.CorpusError, message): + corpus.load_corpus(self.root) + for command in ("check", "index", "search"): + for to_file in (False, True): + with self.subTest(command=command, to_file=to_file): + output = self.root / "result.json" + args = [command, "--root", str(self.root)] + if command == "search": + args += ["payment", "--brand", "other"] + if to_file: + args += ["--output", str(output)] + with mock.patch("sys.stdout", new_callable=io.StringIO) as stdout, \ + mock.patch("sys.stderr", new_callable=io.StringIO) as stderr: + self.assertEqual(corpus.main(args), 1) + self.assertEqual(stdout.getvalue(), "") + self.assertIn(message, stderr.getvalue()) + self.assertNotIn("Traceback", stderr.getvalue()) + self.assertFalse(output.exists()) + + def test_ingestion_limits_exact_boundary_unchanged_control(self): + self.put("a.md") + self.put("nested/b.md") + self.put("README.md", "ignored") + self.put("notes.txt", "ignored") + expected = corpus.load_corpus(self.root) + size = len(BASE.encode("utf-8")) + with mock.patch.multiple(corpus, MAX_REFERENCE_FILES=2, + MAX_REFERENCE_BYTES=size, + MAX_TOTAL_REFERENCE_BYTES=2 * size, + MAX_DIRECTORY_DEPTH=1, MAX_FILESYSTEM_ENTRIES=5): + self.assertEqual(corpus.load_corpus(self.root), expected) + with mock.patch("sys.stdout", new_callable=io.StringIO) as stdout: + self.assertEqual(corpus.main(["index", "--root", str(self.root)]), 0) + self.assertEqual(json.loads(stdout.getvalue())["records"], + [corpus.public_record(record) for record in expected]) + + def test_reference_file_count_limit(self): + self.put("a.md") + self.put("b.MD") + self.assert_limit_failure("MAX_REFERENCE_FILES", 1, "reference file count limit exceeded") + + def test_reference_byte_limit(self): + self.put(text=BASE.replace("Sample", "Café")) + size = len((self.root / "references" / "voice.md").read_bytes()) + self.assert_limit_failure("MAX_REFERENCE_BYTES", size - 1, "reference byte limit exceeded") + + def test_total_reference_byte_limit(self): + self.put("a.md") + self.put("b.md") + self.assert_limit_failure("MAX_TOTAL_REFERENCE_BYTES", 2 * len(BASE.encode()) - 1, + "total reference byte limit exceeded") + + def test_directory_depth_limit_including_ignored_contents(self): + self.put("a.md") + self.put("one/two/notes.txt", "ignored") + self.assert_limit_failure("MAX_DIRECTORY_DEPTH", 1, "directory depth limit exceeded") + + def test_filesystem_entry_limit_including_ignored_contents(self): + self.put("a.md") + self.put("nested/README.md", "ignored") + self.put("notes.txt", "ignored") + self.assert_limit_failure("MAX_FILESYSTEM_ENTRIES", 3, "filesystem entry limit exceeded") + + def test_reference_read_is_bounded_and_checks_actual_bytes(self): + self.put() # The on-disk size is smaller than the bytes supplied by the stream. + size = len(BASE.encode()) + stream = mock.Mock(wraps=io.BytesIO(BASE.encode() + b"extra")) + context = mock.MagicMock() + context.__enter__.return_value = stream + with mock.patch.object(corpus, "MAX_REFERENCE_BYTES", size), \ + mock.patch.object(Path, "open", return_value=context), \ + self.assertRaisesRegex(corpus.CorpusError, "reference byte limit exceeded"): + corpus.load_corpus(self.root) + stream.read.assert_called_once_with(size + 1) + + def test_hash_and_parse_use_same_actual_bytes(self): + self.put() + raw = BASE.replace("Sample", "Café").encode("utf-8") + stream = mock.Mock(wraps=io.BytesIO(raw)) + context = mock.MagicMock() + context.__enter__.return_value = stream + with mock.patch.object(corpus, "MAX_REFERENCE_BYTES", len(raw)), \ + mock.patch.object(corpus, "MAX_TOTAL_REFERENCE_BYTES", len(raw)), \ + mock.patch.object(Path, "open", return_value=context): + record = corpus.load_corpus(self.root)[0] + stream.read.assert_called_once_with(len(raw) + 1) + self.assertEqual(record["metadata"]["title"], "Café") + self.assertEqual(record["bytes"], len(raw)) + self.assertEqual(record["sha256"], hashlib.sha256(raw).hexdigest()) + def test_search_and_exact_filters(self): self.put("a.md") self.put("b.md", BASE.replace('"shared"', '"support"').replace('"should"', '"must"')) diff --git a/tests/test_receipt.py b/tests/test_receipt.py index 83c64cc..277ebc8 100644 --- a/tests/test_receipt.py +++ b/tests/test_receipt.py @@ -207,12 +207,31 @@ def test_cli_control_and_negative_leave_all_files_unchanged(self): self.assertEqual(result.returncode, 0 if valid else 1, result.stderr) if valid: self.assertEqual(json.loads(result.stdout)["overall"], 1) - self.assertEqual(result.stderr, "") + self.assertIn("evidence and approval unverified", result.stderr) else: self.assertEqual(result.stdout, "") self.assertIn("require evidence", result.stderr) self.assertEqual({p.name: p.read_bytes() for p in root.iterdir()}, before) + def test_unverified_high_score_and_approval_are_not_attestation(self): + self.value["reviewer_approval"] = "approved; fictional unsupported claim" + self.value["evidence"][0]["locator"] = "nonexistent:fictional-evidence" + for dimension in self.value["dimensions"].values(): + dimension.update(score=3, evidence_ids=["e1"]) + self.assertEqual(receipt.validate(self.value)["overall"], 3) + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "receipt.json" + for blocked in (False, True): + self.value["blockers"] = ["Required source unavailable."] if blocked else [] + path.write_text(json.dumps(self.value), encoding="utf-8") + result = subprocess.run([sys.executable, "-B", str(TOOL), str(path)], + capture_output=True, text=True) + self.assertEqual(result.returncode, 0) + self.assertEqual(json.loads(result.stdout)["overall"], 0 if blocked else 3) + self.assertIn("structure and arithmetic valid", result.stderr) + self.assertIn("evidence and approval unverified", result.stderr) + self.assertIn("exit 0 is not artifact approval", result.stderr) + def test_cli_invalid_json_encoding_and_missing_path(self): with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "receipt.json" diff --git a/tools/README.md b/tools/README.md index 8a5805e..59c11a6 100644 --- a/tools/README.md +++ b/tools/README.md @@ -20,6 +20,20 @@ python3 tools/corpus.py --help `--root` selects a directory containing `references/`. To check your own system, use `--root .`. Flat and nested Markdown files are included; README placeholders, calibration pairs, and files outside `references/` are not. All symlinks and special files inside references are rejected, as are symlinks in the selected root path. Use a real directory path, not a symlink. Do not modify the corpus concurrently while checking it; this is not a filesystem sandbox. +## Fixed ingestion limits + +Every command rejects a corpus exceeding any of these limits: + +- 1,000 reference Markdown files (README placeholders are excluded). +- 1 MiB (1,048,576 bytes) per reference file. +- 16 MiB (16,777,216 bytes) of reference file contents in total. +- 16 directory levels below `references/`, which is level 0. +- 10,000 filesystem entries below `references/`, counting directories, README placeholders, and non-Markdown files as well as references. The `references/` directory itself is not counted. + +These are fixed limits, not CLI options. Ignored files count toward the traversal limit but not the reference file or byte limits. Each reference read is bounded to the per-file limit plus one byte; byte checks, parsing, and SHA-256 use the bytes actually read, not a file-size estimate. Exactly reaching a limit is allowed. Exceeding any limit fails visibly with exit status `1`, before JSON output or output-file creation; no partial results are returned. + +Limits bound ingestion, not all process resources. They do not make this tool a filesystem sandbox or safe against concurrent changes. Do not run it against a corpus being modified concurrently. + ## What the output means - **Check:** structural validity and file count. A valid empty template reports `count: 0`; it contains no writing guidance. @@ -63,7 +77,7 @@ Drafting and review now include a [traceability and confidence receipt](../docs/ python3 tools/receipt.py path/to/receipt.json ``` -It reads one local JSON file and emits the validated receipt with its computed `overall` to stdout. It never writes back, follows source/evidence locators, verifies hashes, or calls a model. Exit 0 means the receipt is structurally valid—even when `overall` is 0 (blocked). Exit 1 means invalid input; 2 means CLI misuse. Do not use process success as artifact approval. +It reads one local JSON file and emits the validated receipt with its computed `overall` to stdout. On success, a notice on stderr labels the result as structure/arithmetic validation with unverified evidence and approval. The JSON shape is unchanged; integrations must preserve that distinction even if they discard stderr. It never writes back, follows source/evidence locators, verifies hashes, or calls a model. Exit 0 means the receipt is structurally valid—even when `overall` is 0 (blocked). Exit 1 means invalid input; 2 means CLI misuse. Do not use process success as artifact approval. The author supplies evidence-backed dimension assessments. The checker computes the lowest applicable score, with any declared blocker forcing 0. It rejects invalid scores, duplicate keys and IDs within each record group, dangling links, missing reasons, and inconsistent supplied totals. It cannot detect invented evidence, omitted blockers, unjustified N/A, or misleading reasoning. Evidence adequacy still needs review. diff --git a/tools/corpus.py b/tools/corpus.py index cd7b016..634db24 100644 --- a/tools/corpus.py +++ b/tools/corpus.py @@ -4,6 +4,7 @@ import argparse import hashlib import json +import os from pathlib import Path import re import stat @@ -14,6 +15,13 @@ REQUIRED = {"title", "brand", "domain", "enforcement"} ALLOWED = REQUIRED | {"teaching"} +# Fixed ingestion limits, not CLI options. Bytes refer to original file contents. +MAX_REFERENCE_FILES = 1000 +MAX_REFERENCE_BYTES = 1024 * 1024 +MAX_TOTAL_REFERENCE_BYTES = 16 * 1024 * 1024 +MAX_DIRECTORY_DEPTH = 16 +MAX_FILESYSTEM_ENTRIES = 10000 + class CorpusError(ValueError): """Invalid corpus input.""" @@ -108,24 +116,52 @@ def load_corpus(root): if not references.exists() and not references.is_symlink(): raise CorpusError("references/ does not exist under the selected root") records = [] + entry_count = 0 + reference_count = 0 + total_bytes = 0 - def walk(directory): + def walk(directory, depth=0): + nonlocal entry_count, reference_count, total_bytes + if depth > MAX_DIRECTORY_DEPTH: + raise CorpusError("directory depth limit exceeded (%d): %s" % + (MAX_DIRECTORY_DEPTH, directory.relative_to(root).as_posix())) mode = directory.lstat().st_mode if stat.S_ISLNK(mode): raise CorpusError("symlink not allowed: " + directory.relative_to(root).as_posix()) if not stat.S_ISDIR(mode): raise CorpusError("expected directory: " + directory.relative_to(root).as_posix()) - for child in sorted(directory.iterdir(), key=lambda item: item.name): + # Bound enumeration before sorting; ignored files also consume the budget. + children = [] + with os.scandir(directory) as entries: + for entry in entries: + entry_count += 1 + if entry_count > MAX_FILESYSTEM_ENTRIES: + raise CorpusError("filesystem entry limit exceeded (%d) under references/" % + MAX_FILESYSTEM_ENTRIES) + children.append(directory / entry.name) + for child in sorted(children, key=lambda item: item.name): path = child.relative_to(root).as_posix() mode = child.lstat().st_mode if stat.S_ISLNK(mode): raise CorpusError("symlink not allowed: " + path) if stat.S_ISDIR(mode): - walk(child) + walk(child, depth + 1) elif not stat.S_ISREG(mode): raise CorpusError("special file not allowed: " + path) elif child.suffix.lower() == ".md" and child.name.lower() != "readme.md": - raw = child.read_bytes() + reference_count += 1 + if reference_count > MAX_REFERENCE_FILES: + raise CorpusError("reference file count limit exceeded (%d): %s" % + (MAX_REFERENCE_FILES, path)) + with child.open("rb") as stream: + raw = stream.read(MAX_REFERENCE_BYTES + 1) + if len(raw) > MAX_REFERENCE_BYTES: + raise CorpusError("reference byte limit exceeded (%d): %s" % + (MAX_REFERENCE_BYTES, path)) + total_bytes += len(raw) + if total_bytes > MAX_TOTAL_REFERENCE_BYTES: + raise CorpusError("total reference byte limit exceeded (%d): %s" % + (MAX_TOTAL_REFERENCE_BYTES, path)) try: metadata, body, first_line = parse(raw, path) except CorpusError as exc: @@ -180,6 +216,14 @@ def search(records, query, filters): YAML aliases/tags, BOM, and other YAML features are unsupported and rejected. Output and limits: + Fixed ingestion caps: 1000 reference Markdown files; 1 MiB (1048576 bytes) + per reference; 16 MiB (16777216 bytes) total reference contents; 16 directory + levels below references/ (which is level 0); 10000 filesystem entries below + references/, including directories, README placeholders, and non-Markdown files. + Ignored files do not count toward reference file/byte caps. Reads are bounded + to the per-file cap plus one byte. Exceeding any cap fails before JSON output + or output-file creation; no partial results. Limits are not a sandbox and do + not make concurrent corpus modification safe. JSON schema_version 1; paths relative to root, sorted by path, no timestamps or absolute paths. SHA-256 covers original file bytes, including frontmatter. Index is a manifest, not a database; search always rereads and validates files. diff --git a/tools/receipt-format.md b/tools/receipt-format.md index 2774907..0e3aa75 100644 --- a/tools/receipt-format.md +++ b/tools/receipt-format.md @@ -2,6 +2,8 @@ Plain-text receipts remain the default user interface. This optional JSON representation supports local integrations with `python3 tools/receipt.py FILE.json`. The [traceability contract](../docs/traceability.md) governs meaning; the checker validates structure and arithmetic only. +On successful validation, JSON remains on stdout unchanged in shape. A notice on stderr states that only structure and arithmetic were validated; evidence and approval remain unverified. Integrations must preserve this distinction even if they discard stderr. Exit 0 also covers blocked receipts. + All fields below are required except `sha256` and `overall`. Unknown fields are rejected. Strings must be nonempty; lists may be empty where there is nothing to report. An empty list is an explicit claim of absence, not a way to hide unknowns. | Field | Shape and meaning | diff --git a/tools/receipt.py b/tools/receipt.py index cd3a1da..82104b2 100644 --- a/tools/receipt.py +++ b/tools/receipt.py @@ -148,6 +148,8 @@ def main(argv=None): except (OSError, ValueError, RecursionError) as exc: print("receipt: " + str(exc), file=sys.stderr) return 1 + print("receipt: structure and arithmetic valid; evidence and approval unverified; " + "exit 0 is not artifact approval", file=sys.stderr) print(json.dumps(result, indent=2, sort_keys=True, ensure_ascii=True, allow_nan=False)) return 0 diff --git a/website/README.md b/website/README.md index e45b490..9d1ac18 100644 --- a/website/README.md +++ b/website/README.md @@ -1,6 +1,6 @@ # Chalkline explainer site -Static public-site proposal. No build, dependencies, accounts, uploads, remote assets, analytics, or live model. GitHub Pages publishes the site after approved changes merge to `main`. +Static public explainer. No build, dependencies, accounts, uploads, remote assets, analytics, or live model. GitHub Pages publishes the site after approved changes merge to `main`. From the repository root, preview with: @@ -14,7 +14,7 @@ Open http://127.0.0.1:8000. Python is optional; any static server works. ## Demo boundaries -Harbor and its tasks are fictional. The demo does not run AI, verify facts, consult external references, or record reviewer approval. Its confidence assessment is illustrative and defaults to limited support. Removing a required source withholds the candidate and sets overall confidence to zero. Restoring it shows the candidate again, without implying approval. The site labels receipt functionality as proposed in PR #18 while that work is unmerged. +Harbor and its tasks are fictional. The demo does not run AI, verify facts, consult external references, or record reviewer approval. Its confidence assessment is illustrative and defaults to limited support. Removing a required source withholds the candidate and sets overall confidence to zero. Restoring it shows the candidate again, without implying approval. The receipt contract and checker are part of the repository; the site illustrates them without running the checker or a model. ## Checks @@ -25,7 +25,7 @@ node website/test.cjs Node is needed only for these optional developer checks. The test exercises the actual script using a small DOM stub: both tasks, source removal/restoration, minimum scoring, and approval separation. It does not replace browser or accessibility review. -Browser checks on the local prototype covered desktop and 390px mobile layout, missing-source behavior, restoration, and absence of off-origin resources. No full accessibility audit, screen-reader test, live-model evaluation, or publication review is claimed. Recheck links and PR status before publication. Keep the site explanation aligned with canonical repository contracts; illustrative scores and process exit status never authorize publication. +Browser checks on the local prototype covered desktop and 390px mobile layout, missing-source behavior, restoration, and absence of off-origin resources. No full accessibility audit, screen-reader test, live-model evaluation, or publication review is claimed. Recheck links and browser behavior before publication. Keep the site explanation aligned with canonical repository contracts; illustrative scores and process exit status never authorize publication. ## Publishing diff --git a/website/index.html b/website/index.html index 3eff053..7d97edb 100644 --- a/website/index.html +++ b/website/index.html @@ -3,7 +3,7 @@ - + Chalkline — a writing system your team owns @@ -22,7 +22,7 @@

A writing system your team owns.

Chalkline helps your team write down its voice, terminology, and channel rules so agents can consult them before drafting. You review the rules and keep them in editable Markdown files.

Set up your writing system Read the repository ↗
-
Local site prototype

This local prototype has no live model or uploads. The receipt example shows proposed work in PR #18, not a merged release.

+
Static explainer

This site has no live model or uploads. The receipt example illustrates the traceability contract; it does not verify a draft.

02 / Drift

Keep writing guidance out of scattered prompts

@@ -61,7 +61,7 @@

A writing system your team owns.

If a draft reveals missing or unclear guidance, propose a reference change for the responsible person to review. An accepted edit does not automatically become a rule for everyone. Test changes on new, comparable work without showing the agent the expected rewrite; the sample used during setup cannot establish whether the guidance helps on other tasks.

Get started

Use the setup protocol and fictional examples in the repository to build your own references, with reviewers chosen by your team.

Explore Chalkline on GitHub
- + diff --git a/website/test.cjs b/website/test.cjs index e0a2567..31e528d 100644 --- a/website/test.cjs +++ b/website/test.cjs @@ -43,3 +43,7 @@ for (const match of html.matchAll(/(?:src|href)="([^"#]+)"/g)) { assert.ok(fs.existsSync(`${__dirname}/${match[1]}`), `Missing local asset ${match[1]}`); } console.log('PASS: both tasks, blocked/restored states, approval separation, local assets, and natural wrapping.'); + +assert.doesNotMatch(html, /proposed work in PR #18|not a merged release|PR #18: proposed receipt work/); +assert.match(html, /docs\/traceability\.md/); +assert.match(html, /docs\/sharing-readiness\.md/);