mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-08 08:11:17 -10:00
placeholder-suppression completeness (release-0.4.7): audit mini-reqs arm + INTENT section join the one scaffold grammar
Micro-bundle, three items, zero new copy: - A: twin module gains hasAuthoredNumberedItem (dot-or-paren + non-placeholder, the ONE authored-numbered-item grammar); compose hasAuthoredMiniReqs delegates (single-parse pin governs the parse; the twin fn is the line grammar); both scope-audit mini-reqs arms consume it — healing the audit's placeholder-blindness AND the dot/paren grammar divergence (a live audit-absent/review-present R3 case for '1)' items) in one predicate. Finding message unchanged. - B: twin module gains isPlaceholderOnlyBlock (per-line; null/empty -> false); a placeholder-only Intent section is treated as absent at the source so the existing 'no intent recorded' degrade fires. No new string; UI + freeze inherit. - C: prose/bullet-only mini-reqs suppression gets its test pin + doc-comment disclosure (reviewer-L1 ruling); zero behavior change, asserted on base behavior first. Twin pairs byte-identical (parity-pinned via existing CLASSIFIER_FILES). Tests: unit vectors (block + numbered-item grammars), audit-arm matrix, cross-reader agreement incl. the paren-heal shown RED at base (disclosed expected-change), intent degrade + byte-identity carve, suppression pins. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
1 parent
b8c1153559
commit
8757593fe2
8 files changed
+273
-13
No files matched your search
@@ -27,6 +27,41 @@ export function isScaffoldPlaceholderText(text: string): boolean {
|
||||
return /^\[.*\]$/.test(text.trim());
|
||||
}
|
||||
|
||||
/** release-0.4.7 placeholder-suppression completeness — true when `body`
|
||||
* carries at least one numbered item (`1.` OR `1)` form) whose text is NOT a
|
||||
* scaffold placeholder: the ONE authored-numbered-item grammar. Consumers:
|
||||
* review compose `hasAuthoredMiniReqs` + BOTH scope-audit mini-reqs arms —
|
||||
* one predicate heals the IF-3 dot/paren grammar divergence and the audit
|
||||
* arm's placeholder-blindness together (arch MB-AR-1 primary shape).
|
||||
* Prose-only or bullet-only bodies are deliberately NOT authored — the
|
||||
* numbered tier is where approval starts (reviewer-L1 ruling 2026-07-11,
|
||||
* intent-stage gate). `null` → false. */
|
||||
export function hasAuthoredNumberedItem(body: string | null): boolean {
|
||||
if (body === null) return false;
|
||||
for (const line of body.split("\n")) {
|
||||
const m = line.match(/^\s*\d+[.)]\s+(.+)$/);
|
||||
if (m && !isScaffoldPlaceholderText(m[1]!.trim())) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** release-0.4.7 placeholder-suppression completeness — true when EVERY
|
||||
* non-blank line of `block` is a scaffold placeholder: the section-level
|
||||
* "nothing authored here" test. Per-LINE deliberately, not a whole-string
|
||||
* trim — a two-line `[a]\n[b]` body is placeholder-only per line but would
|
||||
* fail the single-line regex (the multi-line honest case, arch MB-AR-2).
|
||||
* `null`/empty/blank-only → false: absence is its own state; callers keep
|
||||
* their existing null handling. */
|
||||
export function isPlaceholderOnlyBlock(block: string | null): boolean {
|
||||
if (block === null) return false;
|
||||
const lines = block
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => l.length > 0);
|
||||
if (lines.length === 0) return false;
|
||||
return lines.every((l) => isScaffoldPlaceholderText(l));
|
||||
}
|
||||
|
||||
/** The generic acceptance triple scaffolded by
|
||||
* `packages/cli/src/lib/scope-templates/slice-progress.md` — exact trimmed
|
||||
* literals, sync-tested against the shipped template so constant/template
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as YAML from "yaml";
|
||||
import { isMissionDotId, isSliceDotId } from "./dot-id.js";
|
||||
import { isScaffoldPlaceholderText } from "./scaffold-placeholder.js";
|
||||
import { isScaffoldPlaceholderText, hasAuthoredNumberedItem } from "./scaffold-placeholder.js";
|
||||
|
||||
export type RailStatus = "present" | "missing" | "malformed" | "readme-only";
|
||||
export type FindingSeverity = "high" | "medium" | "low" | "info";
|
||||
@@ -420,7 +420,12 @@ export function classifyScopeItem(input: ScopeAuditInput): ScopeAuditResult {
|
||||
// at least one numbered list item; a heading over prose-only is
|
||||
// malformed (no usable requirements projection).
|
||||
const miniBody = h2Body(contractSource.content, "Mini-requirements");
|
||||
const hasNumberedItem = miniBody !== null && /^\s*\d+\.\s+\S/m.test(miniBody);
|
||||
// release-0.4.7 micro-bundle A: an AUTHORED numbered item — the twin
|
||||
// module's ONE authored-numbered-item grammar, shared with review
|
||||
// compose (heals the dot/paren grammar split AND the placeholder
|
||||
// blindness in one predicate; the finding message below already reads
|
||||
// honestly for both and stays unchanged).
|
||||
const hasNumberedItem = hasAuthoredNumberedItem(miniBody);
|
||||
if (!hasNumberedItem) {
|
||||
findings.push({
|
||||
kind: "mini_requirements_missing_or_malformed",
|
||||
|
||||
@@ -49,7 +49,11 @@ import {
|
||||
type VerdictTone,
|
||||
type VerifyLineage,
|
||||
} from "./types.js";
|
||||
import { isScaffoldPlaceholderText } from "../scope/scaffold-placeholder.js";
|
||||
import {
|
||||
isScaffoldPlaceholderText,
|
||||
hasAuthoredNumberedItem,
|
||||
isPlaceholderOnlyBlock,
|
||||
} from "../scope/scaffold-placeholder.js";
|
||||
|
||||
// --- Fixed, visible v1 thresholds (markdown-steered tuning is a named fast-follow) ---
|
||||
export const IDLE_WITH_WORK_THRESHOLD_MIN = 30;
|
||||
@@ -343,14 +347,16 @@ export function extractMiniReqs(prd: string | null): string | null {
|
||||
* SINGLE-PARSE PIN (arch AR-2): the derivePhase `prdAuthored` signal AND the
|
||||
* PLAN concise render decision MUST both derive from this ONE parse of the
|
||||
* ONE extractMiniReqs extraction — a second mini-reqs grammar anywhere in
|
||||
* this file would recreate the seam map's R3 divergence class intra-file. */
|
||||
* this file would recreate the seam map's R3 divergence class intra-file.
|
||||
*
|
||||
* release-0.4.7 micro-bundle A: the LINE grammar now lives in the twin
|
||||
* module's `hasAuthoredNumberedItem` (shared with both scope-audit mini-reqs
|
||||
* arms — the IF-3 dot/paren heal, arch MB-AR-1); this function keeps the
|
||||
* parse-side contract above and delegates the grammar. Prose/bullet-only is
|
||||
* deliberately not-authored — reviewer-L1 ruling 2026-07-11, intent-stage
|
||||
* gate: the numbered tier is where approval starts. */
|
||||
export function hasAuthoredMiniReqs(miniReqs: string | null): boolean {
|
||||
if (miniReqs === null) return false;
|
||||
for (const line of miniReqs.split("\n")) {
|
||||
const m = line.match(/^\s*\d+[.)]\s+(.+)$/);
|
||||
if (m && !isScaffoldPlaceholderText(m[1]!.trim())) return true;
|
||||
}
|
||||
return false;
|
||||
return hasAuthoredNumberedItem(miniReqs);
|
||||
}
|
||||
|
||||
/** One promised deliverable from the D2 `## Proof contract` (§3.1): the item
|
||||
@@ -1027,7 +1033,13 @@ export function composeSliceReview(inputs: SliceComposeInputs): ComposedSliceRev
|
||||
const deliveredLock = lockFrom(inputs.approval.delivery);
|
||||
|
||||
const escaping = new Set<string>();
|
||||
const intentText = extractSection(inputs.readme, "Intent");
|
||||
// release-0.4.7 micro-bundle B: a placeholder-only Intent section (the
|
||||
// shipped template scaffolds it as one fully-bracket-wrapped line) is
|
||||
// treated as ABSENT at the source, so the EXISTING "no intent recorded"
|
||||
// degrade fires — no new string, UI/freeze inherit (R7). Any authored line
|
||||
// in the block keeps the section verbatim (byte-identity carve).
|
||||
const intentTextRaw = extractSection(inputs.readme, "Intent");
|
||||
const intentText = intentTextRaw !== null && !isPlaceholderOnlyBlock(intentTextRaw) ? intentTextRaw : null;
|
||||
const intentMedia = sectionMedia(intentText, escaping);
|
||||
const miniReqs = extractMiniReqs(inputs.prd);
|
||||
const planMedia = dedupMedia([
|
||||
|
||||
@@ -27,6 +27,41 @@ export function isScaffoldPlaceholderText(text: string): boolean {
|
||||
return /^\[.*\]$/.test(text.trim());
|
||||
}
|
||||
|
||||
/** release-0.4.7 placeholder-suppression completeness — true when `body`
|
||||
* carries at least one numbered item (`1.` OR `1)` form) whose text is NOT a
|
||||
* scaffold placeholder: the ONE authored-numbered-item grammar. Consumers:
|
||||
* review compose `hasAuthoredMiniReqs` + BOTH scope-audit mini-reqs arms —
|
||||
* one predicate heals the IF-3 dot/paren grammar divergence and the audit
|
||||
* arm's placeholder-blindness together (arch MB-AR-1 primary shape).
|
||||
* Prose-only or bullet-only bodies are deliberately NOT authored — the
|
||||
* numbered tier is where approval starts (reviewer-L1 ruling 2026-07-11,
|
||||
* intent-stage gate). `null` → false. */
|
||||
export function hasAuthoredNumberedItem(body: string | null): boolean {
|
||||
if (body === null) return false;
|
||||
for (const line of body.split("\n")) {
|
||||
const m = line.match(/^\s*\d+[.)]\s+(.+)$/);
|
||||
if (m && !isScaffoldPlaceholderText(m[1]!.trim())) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** release-0.4.7 placeholder-suppression completeness — true when EVERY
|
||||
* non-blank line of `block` is a scaffold placeholder: the section-level
|
||||
* "nothing authored here" test. Per-LINE deliberately, not a whole-string
|
||||
* trim — a two-line `[a]\n[b]` body is placeholder-only per line but would
|
||||
* fail the single-line regex (the multi-line honest case, arch MB-AR-2).
|
||||
* `null`/empty/blank-only → false: absence is its own state; callers keep
|
||||
* their existing null handling. */
|
||||
export function isPlaceholderOnlyBlock(block: string | null): boolean {
|
||||
if (block === null) return false;
|
||||
const lines = block
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => l.length > 0);
|
||||
if (lines.length === 0) return false;
|
||||
return lines.every((l) => isScaffoldPlaceholderText(l));
|
||||
}
|
||||
|
||||
/** The generic acceptance triple scaffolded by
|
||||
* `packages/cli/src/lib/scope-templates/slice-progress.md` — exact trimmed
|
||||
* literals, sync-tested against the shipped template so constant/template
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as YAML from "yaml";
|
||||
import { isMissionDotId, isSliceDotId } from "./dot-id.js";
|
||||
import { isScaffoldPlaceholderText } from "./scaffold-placeholder.js";
|
||||
import { isScaffoldPlaceholderText, hasAuthoredNumberedItem } from "./scaffold-placeholder.js";
|
||||
|
||||
export type RailStatus = "present" | "missing" | "malformed" | "readme-only";
|
||||
export type FindingSeverity = "high" | "medium" | "low" | "info";
|
||||
@@ -420,7 +420,12 @@ export function classifyScopeItem(input: ScopeAuditInput): ScopeAuditResult {
|
||||
// at least one numbered list item; a heading over prose-only is
|
||||
// malformed (no usable requirements projection).
|
||||
const miniBody = h2Body(contractSource.content, "Mini-requirements");
|
||||
const hasNumberedItem = miniBody !== null && /^\s*\d+\.\s+\S/m.test(miniBody);
|
||||
// release-0.4.7 micro-bundle A: an AUTHORED numbered item — the twin
|
||||
// module's ONE authored-numbered-item grammar, shared with review
|
||||
// compose (heals the dot/paren grammar split AND the placeholder
|
||||
// blindness in one predicate; the finding message below already reads
|
||||
// honestly for both and stays unchanged).
|
||||
const hasNumberedItem = hasAuthoredNumberedItem(miniBody);
|
||||
if (!hasNumberedItem) {
|
||||
findings.push({
|
||||
kind: "mini_requirements_missing_or_malformed",
|
||||
|
||||
@@ -661,3 +661,59 @@ describe("T7 — byte-identity carve (compose half): authored slices project ide
|
||||
expect(composeSliceReview(baseInputs({ artifacts: fullGate() })).phase).toBe("review");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// release-0.4.7 placeholder-suppression completeness — T-B1 (INTENT section:
|
||||
// placeholder-only projects the existing honest degrade; authored intent
|
||||
// byte-identical) + T-C compose leg (prose/bullet-only mini-reqs suppression
|
||||
// pin — asserts shipped b8c11535 behavior, zero behavior change intended).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("T-B1 — INTENT placeholder-only projects the honest degrade (micro-bundle B)", () => {
|
||||
it("pristine template README (bracket-wrapped Intent) → intent.text null + 'no intent recorded'", () => {
|
||||
const r = composeSliceReview(baseInputs({ readme: pristine.readme, prd: pristine.prd, proofMd: pristine.proof }));
|
||||
expect(r.intent.text).toBeNull();
|
||||
expect(r.intent.degrade).toBe("no intent recorded");
|
||||
});
|
||||
|
||||
it("two-line `[a]\\n[b]` placeholder Intent → degrade (per-line block grammar)", () => {
|
||||
const readme = "---\ntitle: t\n---\n\n# F\n\n## Intent\n\n[a]\n[b]\n";
|
||||
const r = composeSliceReview(baseInputs({ readme }));
|
||||
expect(r.intent.text).toBeNull();
|
||||
expect(r.intent.degrade).toBe("no intent recorded");
|
||||
});
|
||||
|
||||
it("AUTHORED intent projects byte-identically (the carve — filter never eats a real intent)", () => {
|
||||
const r = composeSliceReview(baseInputs());
|
||||
expect(r.intent.text).toContain("The founder's exact words.");
|
||||
expect(r.intent.degrade).toBeNull();
|
||||
const mixed = composeSliceReview(baseInputs({ readme: baseInputs().readme!.replace("The founder's exact words.", "[looks-bracketed] but authored words follow") }));
|
||||
expect(mixed.intent.text).toContain("[looks-bracketed] but authored words follow");
|
||||
});
|
||||
|
||||
it("ABSENT Intent section keeps its own state (null → same degrade, unchanged contract)", () => {
|
||||
const r = composeSliceReview(baseInputs({ readme: "---\nt: 1\n---\n\n# F\n\nno intent heading here\n" }));
|
||||
expect(r.intent.text).toBeNull();
|
||||
expect(r.intent.degrade).toBe("no intent recorded");
|
||||
});
|
||||
});
|
||||
|
||||
describe("T-C — prose/bullet-only mini-reqs suppression pin (reviewer-L1 ruling; shipped behavior)", () => {
|
||||
it("prose-only mini-reqs → PLAN '— not planned yet' (concise text null)", () => {
|
||||
const prd = "## Mini-requirements\n\nJust prose, no numbered tier.\n\n## Proof contract\n\n- [ ] real item\n";
|
||||
const r = composeSliceReview(baseInputs({ prd }));
|
||||
expect(r.plan.concise.text).toBeNull();
|
||||
});
|
||||
|
||||
it("bullet-only mini-reqs → concise text null", () => {
|
||||
const prd = "## Mini-requirements\n\n- bullet one\n- bullet two\n\n## Proof contract\n\n- [ ] real item\n";
|
||||
const r = composeSliceReview(baseInputs({ prd }));
|
||||
expect(r.plan.concise.text).toBeNull();
|
||||
});
|
||||
|
||||
it("mixed prose + ONE authored numbered item → renders", () => {
|
||||
const prd = "## Mini-requirements\n\nContext prose.\n\n1. One real outcome.\n\n## Proof contract\n\n- [ ] real item\n";
|
||||
const r = composeSliceReview(baseInputs({ prd }));
|
||||
expect(r.plan.concise.text).toContain("1. One real outcome.");
|
||||
});
|
||||
});
|
||||
@@ -128,3 +128,50 @@ describe("T5 — shared-helper agreement across the three readers (the R3 pin, p
|
||||
expect(extractProofContract(prd).map((i) => i.text)).toEqual(["phone journey video"]);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// release-0.4.7 placeholder-suppression completeness — T-A1 (audit mini-reqs
|
||||
// arm learns the merged grammar) + T-A2 (the IF-3 paren-grammar HEAL:
|
||||
// RED at base b8c11535 proves today's audit-absent/review-present divergence;
|
||||
// green post-change proves audit joined compose's already-ratified grammar —
|
||||
// an arch-framed expected-change disclosure, not a regression).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function auditFor(prd: string) {
|
||||
return classifyScopeItem({
|
||||
id: "OPR.T.96",
|
||||
path: "/fixture/96-minireqs",
|
||||
readmeFrontmatterRaw: "id: OPR.T.96",
|
||||
progressFileExists: true,
|
||||
readmeOnlyMarker: false,
|
||||
isActiveRelease: true,
|
||||
level: "slice",
|
||||
readmeContent: "# 96\n\n## Intent\n\nauthored\n",
|
||||
implementationPrdContent: prd,
|
||||
});
|
||||
}
|
||||
|
||||
describe("T-A1 — audit mini-reqs arm counts only AUTHORED numbered items", () => {
|
||||
it("placeholder-only mini-reqs → mini_requirements finding FIRES (was: silent)", () => {
|
||||
const audit = auditFor(tpl.prd); // pristine template: `1. [...]` placeholder + placeholder contract
|
||||
expect(audit.findings.map((f) => f.kind)).toContain("mini_requirements_missing_or_malformed");
|
||||
});
|
||||
|
||||
it("authored dot-form mini-reqs → no mini_requirements finding", () => {
|
||||
const prd = tpl.prd.replace(/^1\. \[.*\]$/m, "1. One real observable outcome.");
|
||||
const audit = auditFor(prd);
|
||||
expect(audit.findings.map((f) => f.kind)).not.toContain("mini_requirements_missing_or_malformed");
|
||||
});
|
||||
});
|
||||
|
||||
describe("T-A2 — the IF-3 heal: `1)` paren-form authored items — audit joins compose's grammar", () => {
|
||||
it("paren-form authored mini-reqs: audit finding ABSENT and compose reads authored (agreement)", async () => {
|
||||
const prd = tpl.prd.replace(/^1\. \[.*\]$/m, "1) One real observable outcome.");
|
||||
// Reader 1 — audit: no malformed finding (RED at base: dot-only regex misses `1)`).
|
||||
const audit = auditFor(prd);
|
||||
expect(audit.findings.map((f) => f.kind)).not.toContain("mini_requirements_missing_or_malformed");
|
||||
// Reader 2 — compose: authored TRUE via the same shared grammar.
|
||||
const { extractMiniReqs, hasAuthoredMiniReqs } = await import("../src/domain/review/compose.js");
|
||||
expect(hasAuthoredMiniReqs(extractMiniReqs(prd))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -122,3 +122,68 @@ describe("T6 — GENERIC_SCAFFOLD_ACCEPTANCE stays in sync with the shipped slic
|
||||
expect(checkboxTexts(body!)).toEqual([...GENERIC_SCAFFOLD_ACCEPTANCE]);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// release-0.4.7 placeholder-suppression completeness micro-bundle —
|
||||
// T-B2 (isPlaceholderOnlyBlock unit vectors), T-A grammar
|
||||
// (hasAuthoredNumberedItem unit vectors), T-C (prose/bullet-only suppression
|
||||
// pin — asserts the arch-ruled, reviewer-L1 behavior).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import {
|
||||
hasAuthoredNumberedItem,
|
||||
isPlaceholderOnlyBlock,
|
||||
} from "../src/domain/scope/scaffold-placeholder.js";
|
||||
|
||||
describe("T-B2 — isPlaceholderOnlyBlock (block-level 'nothing authored here')", () => {
|
||||
it("null/empty → false (absence is its own state; callers keep null handling)", () => {
|
||||
expect(isPlaceholderOnlyBlock(null)).toBe(false);
|
||||
expect(isPlaceholderOnlyBlock("")).toBe(false);
|
||||
expect(isPlaceholderOnlyBlock(" \n \n")).toBe(false);
|
||||
});
|
||||
|
||||
it("single fully-bracket-wrapped line → true (the shipped template Intent scaffold)", () => {
|
||||
expect(isPlaceholderOnlyBlock("[The recorded intent, verbatim — what was asked for and why.]")).toBe(true);
|
||||
expect(isPlaceholderOnlyBlock("\n [padded placeholder] \n")).toBe(true);
|
||||
});
|
||||
|
||||
it("multi-line per-LINE case: `[a]\\n[b]` → true (the whole-string trim would miss this)", () => {
|
||||
expect(isPlaceholderOnlyBlock("[a]\n[b]")).toBe(true);
|
||||
});
|
||||
|
||||
it("blank lines are ignored between placeholder lines", () => {
|
||||
expect(isPlaceholderOnlyBlock("[a]\n\n[b]\n")).toBe(true);
|
||||
});
|
||||
|
||||
it("ANY authored line makes the block authored (mixed → false)", () => {
|
||||
expect(isPlaceholderOnlyBlock("[a]\nreal authored words")).toBe(false);
|
||||
expect(isPlaceholderOnlyBlock("The founder's exact words.")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("T-A grammar — hasAuthoredNumberedItem (the ONE authored-numbered-item grammar)", () => {
|
||||
it("dot-form and paren-form authored items both count (`1.` / `1)`)", () => {
|
||||
expect(hasAuthoredNumberedItem("1. Drawer opens from the right side.")).toBe(true);
|
||||
expect(hasAuthoredNumberedItem("1) Drawer opens from the right side.")).toBe(true);
|
||||
});
|
||||
|
||||
it("placeholder-only numbered item → false (template `1. [...]` scaffold)", () => {
|
||||
expect(hasAuthoredNumberedItem("1. [The concise one-glance requirement tier.]")).toBe(false);
|
||||
});
|
||||
|
||||
it("null → false", () => {
|
||||
expect(hasAuthoredNumberedItem(null)).toBe(false);
|
||||
});
|
||||
|
||||
it("T-C: prose-only body → false (reviewer-L1 ruling — deliberately not-authored)", () => {
|
||||
expect(hasAuthoredNumberedItem("Some prose describing intent without structure.")).toBe(false);
|
||||
});
|
||||
|
||||
it("T-C: bullet-only body → false (bullets are not the numbered requirement tier)", () => {
|
||||
expect(hasAuthoredNumberedItem("- bullet item one\n- bullet item two")).toBe(false);
|
||||
});
|
||||
|
||||
it("T-C: mixed prose + one authored numbered item → true", () => {
|
||||
expect(hasAuthoredNumberedItem("Context prose first.\n\n1. One real observable outcome.")).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user