create-pmos 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -0
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/workspace/lib/apply-change.js +51 -1
- package/workspace/public/app.js +122 -200
- package/workspace/public/index.html +6 -3
- package/workspace/public/styles.css +54 -83
- package/workspace/server.js +64 -133
- package/workspace/lib/correction-record.js +0 -127
- package/workspace/lib/finding-contract.js +0 -162
- package/workspace/skills/doc-review.skill +0 -114
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
/* finding-contract — what a review finding must be, and what the PM may do with it.
|
|
2
|
-
* (workspace-review-and-edit · R3/R7)
|
|
3
|
-
*
|
|
4
|
-
* Used on BOTH sides, like preview-kind:
|
|
5
|
-
* - server.js requires it to validate a reviewer skill's output before it reaches the client
|
|
6
|
-
* - the browser loads it as /lib/finding-contract.js so the renderer asks the SAME question
|
|
7
|
-
* so the contract, the endpoint and the UI can never disagree about what is applyable.
|
|
8
|
-
*
|
|
9
|
-
* The one security-critical function is isApplyable(). It decides whether an Apply control
|
|
10
|
-
* exists at all. R7's rule is that a proposal which would close a recorded open question or
|
|
11
|
-
* overwrite a PM decision is surfaced as CONTEXT with no Apply affordance — the absence of the
|
|
12
|
-
* control is the mechanism, and a warning label is explicitly not a substitute. The rubric makes
|
|
13
|
-
* this a hard floor (dim 2 must = 1: zero Apply controls ever rendered for a withheld finding),
|
|
14
|
-
* so this boolean is the whole containment story and it is unit-tested directly.
|
|
15
|
-
*
|
|
16
|
-
* The second load-bearing rule is self_undetectable (R3). A review that emits lint findings
|
|
17
|
-
* becomes a linter the PM stops reading, and "judgement-grade" is not enforceable by inspecting
|
|
18
|
-
* the finding's text. So the contract inverts it: every finding must NAME what the artifact
|
|
19
|
-
* cannot see about itself. A reviewer that cannot fill that line has not found this class of
|
|
20
|
-
* defect, and the finding is rejected structurally rather than by taste.
|
|
21
|
-
*/
|
|
22
|
-
'use strict';
|
|
23
|
-
(function (root, factory) {
|
|
24
|
-
const api = factory();
|
|
25
|
-
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
26
|
-
else root.FindingContract = api;
|
|
27
|
-
})(typeof self !== 'undefined' ? self : this, function () {
|
|
28
|
-
|
|
29
|
-
/* Finding kinds the review must NOT emit (R3). These are owned by the Frontmatter Gate and the
|
|
30
|
-
* linters; re-reporting them here is how counsel decays into noise. Declared by the reviewer
|
|
31
|
-
* and rejected on sight — a blocklist catches the honest mistake, and self_undetectable below
|
|
32
|
-
* catches the dishonest one. */
|
|
33
|
-
const MECHANICAL_KINDS = new Set([
|
|
34
|
-
'frontmatter', 'schema', 'link', 'broken-link', 'missing-path', 'path',
|
|
35
|
-
'string-match', 'regex', 'count', 'occurrence', 'spelling', 'typo', 'formatting', 'lint',
|
|
36
|
-
]);
|
|
37
|
-
|
|
38
|
-
const isStr = (v) => typeof v === 'string' && v.trim().length > 0;
|
|
39
|
-
|
|
40
|
-
/** Reject a finding that a linter should have owned (R3). Kind is declared, not sniffed. */
|
|
41
|
-
function isMechanical(f) {
|
|
42
|
-
return !!f && isStr(f.kind) && MECHANICAL_KINDS.has(f.kind.trim().toLowerCase());
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/* THE guardrail (R7). An Apply control may exist only when the finding is not withheld AND
|
|
46
|
-
* actually carries a change to make. Both halves matter: `withheld` is the PM-decision guard,
|
|
47
|
-
* and a null proposal is the context class (nothing to apply). Anything unexpected — a missing
|
|
48
|
-
* field, a malformed proposal, a non-boolean withheld — resolves to NOT applyable. This
|
|
49
|
-
* function fails closed on purpose; an unparseable finding must never grow a button. */
|
|
50
|
-
function isApplyable(f) {
|
|
51
|
-
if (!f || typeof f !== 'object') return false;
|
|
52
|
-
if (f.withheld === true) return false;
|
|
53
|
-
if (f.withheld !== false) return false; // must be explicitly declared, never inferred
|
|
54
|
-
const p = f.proposal;
|
|
55
|
-
if (!p || typeof p !== 'object') return false;
|
|
56
|
-
return isStr(p.before) || isStr(p.after); // a pure insertion has no `before`
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Validate one finding against the contract. Returns { ok, errors[] } — never throws. */
|
|
60
|
-
function validateFinding(f) {
|
|
61
|
-
const e = [];
|
|
62
|
-
if (!f || typeof f !== 'object') return { ok: false, errors: ['finding is not an object'] };
|
|
63
|
-
|
|
64
|
-
if (!isStr(f.title)) e.push('title is required');
|
|
65
|
-
if (!isStr(f.file)) e.push('file is required');
|
|
66
|
-
if (!isStr(f.argument)) e.push('argument is required — a finding without reasoning is an assertion');
|
|
67
|
-
|
|
68
|
-
// R3: name what the artifact cannot see about itself, or this is not the class of defect
|
|
69
|
-
// this review exists to surface.
|
|
70
|
-
if (!isStr(f.self_undetectable)) {
|
|
71
|
-
e.push('self_undetectable is required — name what this artifact cannot detect about itself (R3)');
|
|
72
|
-
}
|
|
73
|
-
if (isMechanical(f)) {
|
|
74
|
-
e.push(`kind '${f.kind}' is mechanically findable and belongs to lint, not to this review (R3)`);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Evidence is file:line, plural, and cited — an un-sourced finding is a guess.
|
|
78
|
-
if (!Array.isArray(f.evidence) || f.evidence.length === 0) {
|
|
79
|
-
e.push('evidence[] is required — at least one file:line citation');
|
|
80
|
-
} else {
|
|
81
|
-
f.evidence.forEach((ev, i) => {
|
|
82
|
-
if (!ev || typeof ev !== 'object') { e.push(`evidence[${i}] is not an object`); return; }
|
|
83
|
-
if (!isStr(ev.file)) e.push(`evidence[${i}].file is required`);
|
|
84
|
-
if (!Number.isInteger(ev.line) || ev.line < 1) e.push(`evidence[${i}].line must be a positive integer`);
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// withheld is declared, never inferred — the containment guard must be explicit (R7).
|
|
89
|
-
if (typeof f.withheld !== 'boolean') {
|
|
90
|
-
e.push('withheld must be an explicit boolean (R7) — it is never inferred');
|
|
91
|
-
}
|
|
92
|
-
// A withheld finding carries no proposal at all. Carrying one invites a later refactor to
|
|
93
|
-
// "just render it disabled", which is exactly the failure dim 2 forbids.
|
|
94
|
-
if (f.withheld === true && f.proposal != null) {
|
|
95
|
-
e.push('a withheld finding must carry proposal: null — it has no change to offer (R7)');
|
|
96
|
-
}
|
|
97
|
-
if (f.withheld === true && !isStr(f.withheld_reason)) {
|
|
98
|
-
e.push('withheld_reason is required — say why this is yours to settle (R7)');
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (f.proposal != null) {
|
|
102
|
-
const p = f.proposal;
|
|
103
|
-
if (typeof p !== 'object') e.push('proposal must be an object or null');
|
|
104
|
-
else {
|
|
105
|
-
if (p.before != null && typeof p.before !== 'string') e.push('proposal.before must be a string or absent');
|
|
106
|
-
if (!isStr(p.after) && !isStr(p.before)) e.push('proposal must change something (before and/or after)');
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return { ok: e.length === 0, errors: e };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** Validate a whole review payload. One bad finding fails the review — partial trust is worse
|
|
114
|
-
* than none on a surface whose only job is to be believed. */
|
|
115
|
-
function validateReview(r) {
|
|
116
|
-
const e = [];
|
|
117
|
-
if (!r || typeof r !== 'object') return { ok: false, errors: ['review is not an object'] };
|
|
118
|
-
if (!Array.isArray(r.read) || r.read.length === 0) {
|
|
119
|
-
e.push('read[] is required — the review states what it actually read (R2)');
|
|
120
|
-
}
|
|
121
|
-
if (!Array.isArray(r.findings)) {
|
|
122
|
-
e.push('findings[] is required (an empty array is a valid, and good, review — R10)');
|
|
123
|
-
} else {
|
|
124
|
-
r.findings.forEach((f, i) => {
|
|
125
|
-
const v = validateFinding(f);
|
|
126
|
-
if (!v.ok) v.errors.forEach((msg) => e.push(`findings[${i}]: ${msg}`));
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
return { ok: e.length === 0, errors: e };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** Quiet-rate over a scored corpus (R10, rubric dim 1). Documents read with zero findings
|
|
133
|
-
* against them, divided by documents believed clean. The rubric's binding number. */
|
|
134
|
-
function quietRate(reviewed, cleanPaths) {
|
|
135
|
-
const clean = new Set(cleanPaths || []);
|
|
136
|
-
if (clean.size === 0) return null; // no clean class ⇒ no valid precision number
|
|
137
|
-
let quiet = 0;
|
|
138
|
-
for (const p of clean) {
|
|
139
|
-
const noisy = (reviewed || []).some((f) => f && f.file === p);
|
|
140
|
-
if (!noisy) quiet++;
|
|
141
|
-
}
|
|
142
|
-
return quiet / clean.size;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** Action rate (PRD success signal): Apply+Edit ÷ all dispositions. Dismiss-heavy means the
|
|
146
|
-
* review is noise the PM is politely declining, whatever it scored. */
|
|
147
|
-
function actionRate(dispositions) {
|
|
148
|
-
const d = (dispositions || []).filter((x) => x === 'apply' || x === 'edit' || x === 'dismiss');
|
|
149
|
-
if (d.length === 0) return null;
|
|
150
|
-
return d.filter((x) => x !== 'dismiss').length / d.length;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
MECHANICAL_KINDS,
|
|
155
|
-
isMechanical,
|
|
156
|
-
isApplyable,
|
|
157
|
-
validateFinding,
|
|
158
|
-
validateReview,
|
|
159
|
-
quietRate,
|
|
160
|
-
actionRate,
|
|
161
|
-
};
|
|
162
|
-
});
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: doc-review
|
|
3
|
-
label: "Review documents"
|
|
4
|
-
description: "Read the planning documents you name and report only defects the artifact cannot detect about itself — never lint. Each finding carries its evidence and, where one exists, a proposed change you dispose of."
|
|
5
|
-
run: agent
|
|
6
|
-
agent: claude
|
|
7
|
-
output:
|
|
8
|
-
- Read
|
|
9
|
-
- Findings
|
|
10
|
-
- Withheld
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
You are running the "Review documents" skill in this PMOS workspace (local, file mode). Work only
|
|
14
|
-
from this repository's own files, git, and CLIs — no network (D54).
|
|
15
|
-
|
|
16
|
-
Goal: surface defects in the named documents that **the artifact cannot detect about itself**, and
|
|
17
|
-
file them as a contract-checked review the PM disposes of one at a time.
|
|
18
|
-
|
|
19
|
-
## What counts as a finding — and what does not
|
|
20
|
-
|
|
21
|
-
The whole value of this review is that the PM keeps reading it. A review that reports what a linter
|
|
22
|
-
already reports becomes a linter with manners, and gets ignored by Thursday.
|
|
23
|
-
|
|
24
|
-
**Report only defects the document cannot detect about itself.** The four kinds that recur:
|
|
25
|
-
|
|
26
|
-
- **A metric that does not measure its own claim** — a PRD bets on an outcome, then defines success as
|
|
27
|
-
a capability demo, so it can score full marks and move the outcome not at all.
|
|
28
|
-
- **An anchor that cannot score wrong** — a rubric dimension whose anchors award credit for descoping,
|
|
29
|
-
or that cannot distinguish a correct implementation from an absent one.
|
|
30
|
-
- **A gate that scripts away its own question** — a rubric prescribing the Acceptance Gate as the same
|
|
31
|
-
checklist it already scored, turning D12's strategic judgement into a second review pass.
|
|
32
|
-
- **A constraint contradicted elsewhere** — a non-goal or design rule in one shipped document that
|
|
33
|
-
silently rules out what another document proposes.
|
|
34
|
-
|
|
35
|
-
**NEVER report** — these belong to lint and the Frontmatter Gate, and emitting them fails the
|
|
36
|
-
contract outright: frontmatter or schema validity · broken links or missing paths · string/regex
|
|
37
|
-
matches · counting occurrences · spelling, typos, formatting.
|
|
38
|
-
|
|
39
|
-
**Say nothing when there is nothing to say.** An empty findings list is a correct and valued outcome.
|
|
40
|
-
Do not pad a review to look thorough — a false finding costs the PM more than a missed one, because
|
|
41
|
-
they pay for it every time.
|
|
42
|
-
|
|
43
|
-
## The withheld class — where you must NOT propose a fix
|
|
44
|
-
|
|
45
|
-
Some defects are not defects. Before proposing any change, ask: **would applying this close a
|
|
46
|
-
question the PM has parked, or overwrite a decision they recorded?** Check `pm-decisions.md`,
|
|
47
|
-
`DECISIONS.md`, and the document's own history.
|
|
48
|
-
|
|
49
|
-
If yes, the finding is **withheld**: `"withheld": true`, `"proposal": null`, and a
|
|
50
|
-
`"withheld_reason"` saying why it is the PM's to settle. The client renders no Apply control for it —
|
|
51
|
-
the absence of the button is the guard, so a withheld finding carrying a proposal is rejected by the
|
|
52
|
-
contract, not merely discouraged.
|
|
53
|
-
|
|
54
|
-
The recorded instance: `CLAUDE.md` points file-mode state at `pmos/state/*.jsonl`, which looks stale
|
|
55
|
-
and is not — it is a measured open question (`pm-decisions.md` 491–503).
|
|
56
|
-
|
|
57
|
-
## Procedure
|
|
58
|
-
|
|
59
|
-
1. **Take the scope.** Read exactly the documents the PM named. Do not widen it.
|
|
60
|
-
*Done when:* every named path has been read in full, and you can list them.
|
|
61
|
-
|
|
62
|
-
2. **Find, then cut.** For each document, ask the four questions above. Then remove every candidate
|
|
63
|
-
that a regex could have found, and every one you cannot evidence to a specific line.
|
|
64
|
-
*Done when:* each surviving finding names, in one sentence, what the artifact cannot see about
|
|
65
|
-
itself — the `self_undetectable` field. If you cannot write that sentence, drop the finding.
|
|
66
|
-
|
|
67
|
-
3. **Propose, or withhold.** Where a change would genuinely fix it, write `proposal.before` as text
|
|
68
|
-
copied **verbatim** from the file and `proposal.after` as its replacement. `before` must appear in
|
|
69
|
-
the file **exactly once** — the apply path refuses a stale or ambiguous anchor rather than
|
|
70
|
-
guessing. Where the withheld test fires, propose nothing.
|
|
71
|
-
*Done when:* every finding either carries an exact-match proposal or is explicitly withheld.
|
|
72
|
-
|
|
73
|
-
4. **File the review.** Pick a run id (`doc-review-<UTC-timestamp>`), then write
|
|
74
|
-
`workspace/state/reviews/<run-id>.json` with exactly this shape:
|
|
75
|
-
|
|
76
|
-
```json
|
|
77
|
-
{
|
|
78
|
-
"at": "<ISO 8601>",
|
|
79
|
-
"read": ["planning/prd/example-prd.md"],
|
|
80
|
-
"findings": [
|
|
81
|
-
{
|
|
82
|
-
"title": "One sentence, the defect stated plainly.",
|
|
83
|
-
"file": "planning/prd/example-prd.md",
|
|
84
|
-
"kind": "metric-claim-mismatch",
|
|
85
|
-
"argument": "Why this is a defect — the reasoning, not a restatement.",
|
|
86
|
-
"self_undetectable": "What this artifact cannot see about itself.",
|
|
87
|
-
"evidence": [{ "file": "planning/prd/example-prd.md", "line": 17 }],
|
|
88
|
-
"withheld": false,
|
|
89
|
-
"proposal": { "before": "<verbatim text from the file>", "after": "<replacement>" }
|
|
90
|
-
}
|
|
91
|
-
]
|
|
92
|
-
}
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
`withheld` is a required boolean on every finding and is never inferred. A withheld finding sets
|
|
96
|
-
`"proposal": null` and adds `"withheld_reason"`.
|
|
97
|
-
*Done when:* the file is valid JSON and `GET /api/review` returns `ok: true` for it. If it returns
|
|
98
|
-
`422`, the contract rejected a finding — fix it; do not file it anyway.
|
|
99
|
-
|
|
100
|
-
5. **Tell the PM it is ready.** Read `workspace/state/needs.json` (a missing file is `[]`), append
|
|
101
|
-
exactly this object, and write the whole array back — it must stay a valid JSON array:
|
|
102
|
-
|
|
103
|
-
```json
|
|
104
|
-
{"run_id":"<run-id>","kind":"review","initiative_id":"workspace-review-and-edit","question":"Document review — <n> findings"}
|
|
105
|
-
```
|
|
106
|
-
*Done when:* `needs.json` is a valid array containing the new object.
|
|
107
|
-
|
|
108
|
-
## Output contract — produce EXACTLY these sections
|
|
109
|
-
|
|
110
|
-
- **Read** — the documents actually read, one per line.
|
|
111
|
-
- **Findings** — one line per finding: the title and its `file:line`. `None this review.` if empty,
|
|
112
|
-
which is a good outcome, not a failed run.
|
|
113
|
-
- **Withheld** — findings deliberately left for the PM, with the one-line reason. `None this review.`
|
|
114
|
-
if empty.
|