From d719e2d369cd61955d822253dc48d0067d865442 Mon Sep 17 00:00:00 2001 From: Will Curran Date: Mon, 12 Oct 2026 17:15:43 -0700 Subject: [PATCH] PROOF-P1-S3: the document contract, one shape for a proposal, an analysis, and a note skills/doc-artifact is the one contract for what /ck:proof writes from a request. Every kind opens with In one sentence and Decisions for you, and ends with Open questions and What I read. Between them a proposal has Why, What changes, Not doing, What could go wrong, and How we will know it worked; an analysis has The question, Findings, and What it means; a note has The note. A table sets the three kinds side by side: what each is for, what the author does with it, and its size in words. It says which kind a request is, and that a plan is none of them and goes to /ck:plan. The top of the file carries the status in the staged plan's own words, draft or approved. Three sample documents were written to the contract and rendered as proofs. At phone width a file path pushed Done when out of sight in a proposal's table of changes, so Where is its last column. tests/pending/P1-S3.sh moves to tests/checks/. Its five checks are as they were, and eight are added, each for a contract the five pass and that would still be wrong. Co-Authored-By: Claude Opus 5.5 --- skills/doc-artifact/SKILL.md | 133 +++++++++++++++++++++++++++++ tests/checks/P1-S3.sh | 160 +++++++++++++++++++++++++++++++++++ tests/pending/P1-S3.sh | 68 --------------- 3 files changed, 293 insertions(+), 68 deletions(-) create mode 100644 skills/doc-artifact/SKILL.md create mode 100644 tests/checks/P1-S3.sh delete mode 100644 tests/pending/P1-S3.sh diff --git a/skills/doc-artifact/SKILL.md b/skills/doc-artifact/SKILL.md new file mode 100644 index 0000000..0e2da89 --- /dev/null +++ b/skills/doc-artifact/SKILL.md @@ -0,0 +1,133 @@ +--- +name: doc-artifact +description: The Code Katz contract for a document /ck:proof writes from a request, which is a proposal, an analysis, or a note. Every kind opens with one sentence and the decisions the reader must make, and ends with the open questions and what was read. It gives each kind's own sections between, how the three kinds differ, and the checklist. Load when writing, revising, or validating a document under docs/proposal/, docs/analysis/, or docs/note/, inside or outside a ck workflow. +user-invocable: false +--- + +# The document contract (`/docs//-.md`) + +One contract for the three kinds of document `/ck:proof` writes from a request: a proposal, an analysis, and a note. Every kind opens with one sentence and then the decisions the reader must make, and ends with the open questions and then what was read. Only the sections between are the kind's own, so a reader who has seen one proof knows where to look in the next. Plain words throughout. + +`` is `proposal`, `analysis`, or `note`. `` is the day the document was first written, as `YYYY-MM-DD`, and a revision keeps the file's name. + +## The three kinds + +| Kind | What it is for | What the author does with it | Size | +|---|---|---|---| +| Proposal | A change that has not been made | Says yes, no, or what to change | under 2,000 words | +| Analysis | A question, answered from evidence | Decides what to do with the answer | under 2,500 words | +| Note | Something already known or decided, kept so that it is not lost | Checks that it is right | under 800 words | + +The kind is the one the author names. When the author names none, take the first that fits, in this order: a request that ends in a change to make is a proposal, even when most of it is findings; one that ends in an answer is an analysis; one that only keeps what is known is a note. + +A plan is none of the three. A plan is a staged plan: `/ck:plan` writes it, to the `plan-artifact` contract, and `docs/plan/` is its folder. A document with a contract of its own, a brief or a PRD for example, is written to that contract and never to this one. + +## The top of the file + +``` +# : + +Date: . Status: draft, not approved. +``` + +`` is `Proposal`, `Analysis`, or `Note`, as in the path. `Date:` is the date in the file's name. `Status:` is `draft, not approved` until the author approves the document; then it is `approved `, and no decision in it is still open. Nothing else stands above the first heading, so the one sentence stays at the top of the proof. + +## What every kind opens and ends with + +The same two sections open every document, and the same two end it. + +**In one sentence** is exactly one sentence: the whole document, in words the author could say back to someone else. It has no list, no second sentence, and no heading under it. + +**Decisions for you** holds the choices only the author can make. Its first line is `Open: D1, D3.`, naming the decisions still open, or `None open.` Once a document has asked for a decision it has this table, and a decided row stays in it: + +``` +| # | Decision | Choices | Decided | +|---|---|---|---| +| D1 | Where the yearly price is shown | On the plan page (recommended: the question is asked there); in the footer | open | +| D2 | The yearly discount | Two months free (recommended: buyers know the offer); 20 percent | Two months free, 2026-10-12 | +``` + +Choices puts the recommended one first, marked `recommended:` with its reason in a few words. Decided is `open`, or the choice and its day. The reasoning goes in the sections below, so that a row can be answered at a glance. + +**Open questions** holds what nobody knows yet, as a list. Each ends with the assumption the document was written on, after the words `Assumed meanwhile:`. It says `None.` when there are none. A choice the author could make today is a decision, and goes at the top. + +**What I read** lists what the document rests on: every file, by path; every page, by address and the day it was read; every command whose output a claim rests on; and anything the writer watched happen or was told, with the day. Its last item begins `Not checked:` and names what the document relies on that the writer did not read, run, or see. + +A decision, a change, and a finding carry ids: `D1`, `C2`, `F3`. An id is never reused: what is added takes the next number. + +## Proposal (`docs/proposal/-.md`) + +A change that has not been made, put to the author for a yes. It says what will be different and how anyone will know. Who does which part, in what order, and in which session is a staged plan's to say, and `/ck:plan` writes one. + +### Section order + +Use these headings verbatim, in this order. + +1. `## In one sentence`: what would change, and for whom. +2. `## Decisions for you`: the choices that settle whether the change is made, and in what form. +3. `## Why`: what is wrong or missing today, who it costs, and how that is known. One short paragraph. +4. `## What changes`: a table, `| # | Change | Done when | Where |`, one row per change, `C1`, `C2`, and so on. Change is what will be different, in a plain phrase. Done when is something that can be checked. Where is every file the change touches, by path, or the place when it is not a file; the proposal touches nothing else. Where is last, because a path cannot wrap. A row is a change and not a job: it names no owner and no order of work. +5. `## Not doing`: what a reader could expect and will not get, each with the reason. At least one. +6. `## What could go wrong`: a list. Each risk gives the id of the change it comes from, when it comes from one, and what limits it, or says it is accepted. +7. `## How we will know it worked`: at most four results the author can see or run once the change is made. Results, not work done. +8. `## Open questions`: what is still unknown about the change. +9. `## What I read`. + +## Analysis (`docs/analysis/-.md`) + +A question, answered from evidence. It asks for no change of its own: what to do about the answer is the author's decision, at the top. + +### Section order + +Use these headings verbatim, in this order. + +1. `## In one sentence`: the answer. +2. `## Decisions for you`: what the answer asks the author to choose. `None open.` when it only informs. +3. `## The question`: what was asked, what was left out of it, and what would count as an answer. +4. `## Findings`: a list. Each finding is its id and one sentence, in bold, and then its evidence: `**F1. .** Seen: `. The evidence begins with how the finding is known. `Seen:` the writer ran it or watched it happen. `Read:` a file, a page, or a person says so, and is named. `Inferred:` it follows from other findings or from what the writer already knew, and nobody checked it. +5. `## What it means`: the options the findings leave, what each costs, and the one the writer would take. It cites findings by id and claims nothing that no finding supports. +6. `## Open questions`: what the evidence could not settle. +7. `## What I read`. + +## Note (`docs/note/-.md`) + +Something already known or decided, written down so that it is not lost: what happened, what was decided, or how a thing works. It argues for nothing. + +### Section order + +Use these headings verbatim, in this order. + +1. `## In one sentence`: the one thing to remember. +2. `## Decisions for you`: `None open.` in most notes. +3. `## The note`: what a reader who was not there needs, in the order they need it. An event or a decision has its day and who said or decided it. +4. `## Open questions`: what the note leaves unsettled. +5. `## What I read`. + +## On the proof + +The page is made from the markdown alone. + +- The `##` headings are the kind's and no others. Parts of a section go under `###` headings. Both make the page's list of contents, so no two headings are the same, and a `###` heading of 32 characters or fewer keeps to one line of it. +- A file is named by its path, in a code span. A link is for a web address; the page links nothing else. +- A diagram is a `mermaid` fence. The page shows no image and no HTML. + +## Checklist + +1. The file is at `docs//-.md`, and its top is as this contract writes it, with the same kind and date. +2. Every section of its kind is present, in order, with its heading verbatim, and there is no other `##` heading. +3. Each of the kind's own sections holds what its entry under Section order says. +4. In one sentence holds exactly one sentence. +5. Decisions for you begins with `Open:` and the ids still open, or with `None open.`, and its table agrees. Every row has its choices, the recommended one first. An approved document has none open. +6. Every open question has its `Assumed meanwhile:`, or the section says `None.` +7. What I read names every file by path and every page by address and day, and ends with `Not checked:`. +8. No id is used twice, and no two headings are the same. +9. Files are named in code spans, links hold web addresses, and there is no image and no HTML. +10. No key, token, or password: a setting is named, never its value. +11. No hours and no effort estimates. +12. Before `## What I read`, the document is under its kind's size, unless the author asked for more. Count it with `awk '/^## What I read$/{exit} {print}' "" | wc -w`, never by impression. +13. Proposal: every change has a Done when that can be checked, and no row names an owner or an order of work. +14. Analysis: every finding says how it is known, with `Seen:`, `Read:`, or `Inferred:`. + +## Writing + +Plain technical English: name the actor, one instruction per sentence, no filler, no loss of precision. The writer is I and the author is you, as two of the headings say. No em-dashes in prose. diff --git a/tests/checks/P1-S3.sh b/tests/checks/P1-S3.sh new file mode 100644 index 0000000..7060a2f --- /dev/null +++ b/tests/checks/P1-S3.sh @@ -0,0 +1,160 @@ +# tests/checks/P1-S3.sh: the checks for stage P1-S3, Write the document contract. +# +# Written in stage P0-S1, before the work, and moved here when the work was done. Section 21 of +# tests/run.sh says how this file is run and what it may use. Tighten a check when the work shows +# it is too weak. Never loosen one. +# +# The contract's headings were its stage's to choose, so no check here fixes a heading's words. +# The checks hold the shape the proposal fixes: one contract for the three kinds, written the +# way the other contracts are, in which every kind opens with one sentence and then the decisions +# the reader must make, and ends with the open questions and then what was read. +# +# The first five checks are as stage P0-S1 wrote them. The stage added the eight after them, each +# for a contract the first five pass and that would still be wrong: +# - Three lists written under one "Section order" read as one long list, and only the first +# list's opening and the last list's ending are then looked at. So no list names a heading +# twice, and there is one list for all three kinds or one for each, under a heading that +# names its kind. +# - "Opens the same way" was held by one word in each heading. The headings are now the same. +# - A heading for each kind was enough to say how the kinds differ. They now stand side by side. +# - The command counts a document's words, writes it under docs//, and sends a plan to +# /ck:plan. A contract with no size, no path, or a plan among its kinds leaves each of those +# to the command alone. +# - A proof's list of contents is made from these headings. Two headings that the page gives +# one place would send a reader, and a comment, to the wrong section. +# +# Whether its list of headings equals the list the proof command writes to is stage P1-S4's check: +# the command is not written when this stage is done. The rule against an em-dash in prose +# reaches this contract through section 11 of tests/run.sh, which reads every contract. + +# relay : turns a program's lines, "okname" or "failname: why", into +# checks. The program ends with the line "end", so one that stops early is a failed check and not +# a shorter list. +relay() { + local verdict message ended=0 + while IFS=$'\t' read -r verdict message; do + case "$verdict" in ok) ok "$message" ;; fail) fail "$message" ;; end) ended=1 ;; esac + done + (( ended )) || fail "the reading of $1 stopped before its last check" +} + +python3 - skills/doc-artifact/SKILL.md "$PAGE_RENDERER" "$SCRATCH" <<'EOF' | relay "the document contract" +import html, pathlib, re, subprocess, sys +path = pathlib.Path(sys.argv[1]) +text = path.read_text(encoding='utf-8') if path.is_file() else '' +absent = '' if text else f'{path} is not written' +def say(name, why=''): + print(('fail\t' + name + ': ' + why) if why else ('ok\t' + name)) + +front = text.split('---', 2)[1] if text.startswith('---') and text.count('---') >= 2 else '' +lacks = [key for key, pattern in (('name: doc-artifact', r'^name: doc-artifact$'), ('user-invocable: false', r'^user-invocable: false$'), ('a description', r'^description: \S')) + if not re.search(pattern, front, re.M)] +say('the document contract is skills/doc-artifact/SKILL.md, named doc-artifact, described, and not for typing', + absent or ('its frontmatter lacks ' + ', '.join(lacks) if lacks else '')) + +# Each "## Section order" holds the numbered headings of one document, as in every other contract. +# The contract may have one list for all three kinds, or one list for each kind. +orders = [re.findall(r'^\d+\. `## (.+?)`', part.split('\n## Checklist', 1)[0].split('\n# ', 1)[0], re.M) + for part in text.split('## Section order')[1:]] +say('it lists its headings as the other contracts do: numbered, under Section order, with a Checklist after', + absent or ('it has no "## Section order" with numbered `## ` headings' if not orders or not all(orders) + else 'it has no "## Checklist"' if '\n## Checklist' not in text + else 'a list has fewer than four headings' if min(len(o) for o in orders) < 4 else '')) + +listed = bool(orders) and all(len(o) >= 4 for o in orders) +first = [o[:2] for o in orders if not (re.search(r'sentence', o[0], re.I) and re.search(r'decision', o[1], re.I))] if listed else [] +say('every kind opens with one sentence, and then the decisions the reader must make', + absent or ('it has no list of headings to read' if not listed else f'a document opens with {first[0]}' if first else '')) + +last = [o[-2:] for o in orders if not (re.search(r'open question|question.{0,15}\bopen\b', o[-2], re.I) and re.search(r'\bread\b', o[-1], re.I))] if listed else [] +say('every kind ends with the open questions, and then what was read', + absent or ('it has no list of headings to read' if not listed else f'a document ends with {last[0]}' if last else '')) + +# How the kinds differ is said kind by kind: a row, an item, or a heading that begins with the kind, +# or one table with a column for each. +three = ('proposal', 'analysis', 'note') +entry = lambda kind: re.search(r'^[\s|>#*\-\d.`_]*' + kind + r'\b', text, re.I | re.M) +columns = any(all(re.search(r'\|[\s*`_]*' + kind + r'\b', line, re.I) for kind in three) for line in text.split('\n')) +kinds = [] if columns else [kind for kind in three if not entry(kind)] +say('it says how the three kinds differ, with a row, an item, a heading, or a column for a proposal, an analysis, and a note', + absent or ('nothing begins with ' + ', '.join(kinds) if kinds else '')) + +# ─── What the stage added ──────────────────────────────────────────────────── +renderer, scratch = sys.argv[2], pathlib.Path(sys.argv[3]) +lines = text.split('\n') +unread = absent or ('' if listed else 'it has no list of headings to read') + +# The proposal's words are "opens the same way". A word in common is not the same heading. +pairs = sorted({(tuple(o[:2]), tuple(o[-2:])) for o in orders}) +say('every kind opens with the same two headings and ends with the same two, word for word', + unread or ('' if len(pairs) == 1 else f'one list opens with {list(pairs[0][0])} and ends with {list(pairs[0][1])}, ' + f'and another opens with {list(pairs[1][0])} and ends with {list(pairs[1][1])}')) + +twice = sorted({h for o in orders for h in o if o.count(h) > 1}) +say('no list names a heading twice', unread or (f'a list names `## {twice[0]}` more than once' if twice else '')) + +# The heading a list stands under is the nearest one above its "Section order", outside a code fence. +def heading_above(position): + found, fence = '', False + for line in lines[:text.count('\n', 0, position)]: + if line.startswith('```'): fence = not fence + elif not fence and line.startswith('#'): found = line + return found +heads = [heading_above(m.start()) for m in re.finditer('## Section order', text)] +named = [[kind for kind in three if re.search(r'\b' + kind + r'\b', head, re.I)] for head in heads] +say('it has one list of headings for all three kinds, or one for each kind, under a heading that names that kind and no other', + unread or ('' if len(orders) == 1 + else f'it has {len(orders)} lists of headings' if len(orders) != 3 + else '' if sorted(sum(named, [])) == sorted(three) and all(len(n) == 1 for n in named) + else 'its lists stand under ' + ', '.join('"' + head + '"' for head in heads))) + +# Side by side is one table or one list: lines with no blank line between them. +start = r'^[\s|>#*\-\d.`_]*' +together = any(all(re.search(start + kind + r'\b', block, re.I | re.M) for kind in three) for block in re.split(r'\n[ \t]*\n', text)) +say('it sets the three kinds side by side, in one table or one list', + absent or ('' if together or columns else 'no one table or list has a row, an item, or a column for each of the three kinds')) + +# The command counts a document's words. A count is of use only against a size, counted one way. +unsized = [kind for kind in three + if not any(re.search(r'\d[\d,]* words\b', line) for line in lines if re.search(start + kind + r'\b', line, re.I))] +say('it gives each kind a size in words, on a row or an item that begins with the kind, and has the words counted with wc -w', + absent or ('it gives no size in words for ' + ', '.join(unsized) if unsized else '' if 'wc -w' in text else 'it does not say to count with wc -w')) + +say('it says where a document lives: docs//-.md', + absent or ('' if 'docs//-.md' in text else 'it does not name docs//-.md')) + +# A rule is one item of a list, or one paragraph, as the check files for the loop and the command +# read one. Here one sentence of it says a plan is no kind of this contract's, and it names the command. +units, item = [], [] +for line in lines + ['']: + if not line.strip() or re.match(r'(\d+\.|[-*]|#+) ', line): + if item: units.append(' '.join(item)) + item = [] + if line.strip(): item.append(line.strip()) +refuses = lambda unit: any(re.search(r'\bplan\b', sentence, re.I) and re.search(r'\b(none|not|never|no)\b', sentence, re.I) + for sentence in re.split(r'(?<=[.!?])\s+', unit)) +say('it says a plan is none of its kinds, and names /ck:plan for one', + absent or ('' if any('/ck:plan' in unit and refuses(unit) for unit in units) + else 'no item or paragraph of it names /ck:plan and has a sentence that holds plan with none, not, never, or no')) + +# A proof's list of contents is made from a document's headings. So a document with each list's +# headings goes through the page renderer, and each heading must come out once, in order, with a +# place of its own: two headings that differ only in a mark are one place to the page. +problems = [] +for n, order in enumerate(orders if listed else [], 1): + doc, page = scratch / f'list-{n}.md', scratch / f'list-{n}.html' + doc.write_text('# A document\n\n' + ''.join(f'## {heading}\n\nWords.\n\n' for heading in order), encoding='utf-8') + done = subprocess.run([sys.executable, renderer, '--in', str(doc), '--out', str(page), '--title', 'Check'], capture_output=True, text=True) + if done.returncode != 0 or not page.is_file(): + said = (done.stderr or done.stdout).strip().splitlines() + problems.append(f'the renderer made no page for list {n}: ' + (said[-1] if said else 'it said nothing')); continue + contents = re.search(r'', page.read_text(encoding='utf-8'), re.S) + entries = re.findall(r'(.*?)', contents.group(1) if contents else '', re.S) + places, names = [place for place, name in entries], [html.unescape(name) for place, name in entries] + shared = sorted({place for place in places if places.count(place) > 1}) + if names != order: problems.append(f'the page lists {names} for list {n}, and the contract lists {order}') + elif shared: problems.append(f'two headings of list {n} share the place #{shared[0]} on the page') +say("a document written under each list's headings becomes a proof whose list of contents holds each heading once, in the contract's order", + unread or (problems[0] if problems else '')) +print('end') +EOF diff --git a/tests/pending/P1-S3.sh b/tests/pending/P1-S3.sh deleted file mode 100644 index 334c3ce..0000000 --- a/tests/pending/P1-S3.sh +++ /dev/null @@ -1,68 +0,0 @@ -# tests/pending/P1-S3.sh: the checks for stage P1-S3, Write the document contract. -# -# Written in stage P0-S1, before the work. Section 21 of tests/run.sh says how this file is run -# and what it may use. The stage is done when every check here passes and this file has moved to -# tests/checks/. Tighten a check when the work shows it is too weak. Never loosen one. -# -# The contract's headings are this stage's to choose, so no check here fixes a heading's words. -# The checks hold the shape the proposal fixes: one contract for the three kinds, written the -# way the other contracts are, in which every kind opens with one sentence and then the decisions -# the reader must make, and ends with the open questions and then what was read. -# -# Whether its list of headings equals the list the proof command writes to is stage P1-S4's check: -# the command is not written when this stage is done. The rule against an em-dash in prose -# reaches this contract through section 11 of tests/run.sh, which reads every contract. - -# relay : turns a program's lines, "okname" or "failname: why", into -# checks. The program ends with the line "end", so one that stops early is a failed check and not -# a shorter list. -relay() { - local verdict message ended=0 - while IFS=$'\t' read -r verdict message; do - case "$verdict" in ok) ok "$message" ;; fail) fail "$message" ;; end) ended=1 ;; esac - done - (( ended )) || fail "the reading of $1 stopped before its last check" -} - -python3 - skills/doc-artifact/SKILL.md <<'EOF' | relay "the document contract" -import pathlib, re, sys -path = pathlib.Path(sys.argv[1]) -text = path.read_text(encoding='utf-8') if path.is_file() else '' -absent = '' if text else f'{path} is not written' -def say(name, why=''): - print(('fail\t' + name + ': ' + why) if why else ('ok\t' + name)) - -front = text.split('---', 2)[1] if text.startswith('---') and text.count('---') >= 2 else '' -lacks = [key for key, pattern in (('name: doc-artifact', r'^name: doc-artifact$'), ('user-invocable: false', r'^user-invocable: false$'), ('a description', r'^description: \S')) - if not re.search(pattern, front, re.M)] -say('the document contract is skills/doc-artifact/SKILL.md, named doc-artifact, described, and not for typing', - absent or ('its frontmatter lacks ' + ', '.join(lacks) if lacks else '')) - -# Each "## Section order" holds the numbered headings of one document, as in every other contract. -# The contract may have one list for all three kinds, or one list for each kind. -orders = [re.findall(r'^\d+\. `## (.+?)`', part.split('\n## Checklist', 1)[0].split('\n# ', 1)[0], re.M) - for part in text.split('## Section order')[1:]] -say('it lists its headings as the other contracts do: numbered, under Section order, with a Checklist after', - absent or ('it has no "## Section order" with numbered `## ` headings' if not orders or not all(orders) - else 'it has no "## Checklist"' if '\n## Checklist' not in text - else 'a list has fewer than four headings' if min(len(o) for o in orders) < 4 else '')) - -listed = bool(orders) and all(len(o) >= 4 for o in orders) -first = [o[:2] for o in orders if not (re.search(r'sentence', o[0], re.I) and re.search(r'decision', o[1], re.I))] if listed else [] -say('every kind opens with one sentence, and then the decisions the reader must make', - absent or ('it has no list of headings to read' if not listed else f'a document opens with {first[0]}' if first else '')) - -last = [o[-2:] for o in orders if not (re.search(r'open question|question.{0,15}\bopen\b', o[-2], re.I) and re.search(r'\bread\b', o[-1], re.I))] if listed else [] -say('every kind ends with the open questions, and then what was read', - absent or ('it has no list of headings to read' if not listed else f'a document ends with {last[0]}' if last else '')) - -# How the kinds differ is said kind by kind: a row, an item, or a heading that begins with the kind, -# or one table with a column for each. -three = ('proposal', 'analysis', 'note') -entry = lambda kind: re.search(r'^[\s|>#*\-\d.`_]*' + kind + r'\b', text, re.I | re.M) -columns = any(all(re.search(r'\|[\s*`_]*' + kind + r'\b', line, re.I) for kind in three) for line in text.split('\n')) -kinds = [] if columns else [kind for kind in three if not entry(kind)] -say('it says how the three kinds differ, with a row, an item, a heading, or a column for a proposal, an analysis, and a note', - absent or ('nothing begins with ' + ', '.join(kinds) if kinds else '')) -print('end') -EOF