From 093503d833aa85f11da5490b7d416a48e5312ba9 Mon Sep 17 00:00:00 2001 From: "Claude (oddkit project)" Date: Thu, 16 Apr 2026 03:01:06 +0000 Subject: [PATCH 1/3] fix: encode honors context-vs-input governance distinction Server now follows klappy://odd/encoding-types/how-to-write-encoding-types section 'Context vs Input': input generates artifacts; context only informs quality scoring. Before this change, fullInput (input + context) was passed to both the parser and the scorer, causing context paragraphs to become separate standalone artifacts. The governance says context is metadata, not content. Changes: - runEncodeAction: parsers receive input only (not fullInput) - runEncodeAction: scoring receives input + context per artifact so background information still counts toward quality - scoreArtifactQuality: accepts optional scoringText parameter that defaults to artifact.body when not provided - Inline comments cite the governance doc to prevent regression Closes the gap between governance and code surfaced during PR #96 testing. --- workers/src/orchestrate.ts | 42 +++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/workers/src/orchestrate.ts b/workers/src/orchestrate.ts index 783c604..0954fc6 100644 --- a/workers/src/orchestrate.ts +++ b/workers/src/orchestrate.ts @@ -402,15 +402,22 @@ function parseUnstructuredInput(input: string, types: EncodingTypeDef[]): Parsed function scoreArtifactQuality( artifact: ParsedArtifact, criteria: Array<{ criterion: string; check: string; gapMessage: string }>, + scoringText?: string, ): { score: number; maxScore: number; level: string; gaps: string[]; suggestions: string[] } { const gaps: string[] = []; const suggestions: string[] = []; let score = 0; + // Governance: context informs quality scoring. When scoringText is provided + // (artifact.body + context), criteria check against that combined text so + // background information in context (rationale, alternatives, evidence) + // counts toward the artifact's quality without becoming separate artifacts. + // See: klappy://odd/encoding-types/how-to-write-encoding-types#context-vs-input + const text = scoringText ?? artifact.body; if (criteria.length === 0) { - if (artifact.body.split(/\s+/).length >= 10) score++; + if (text.split(/\s+/).length >= 10) score++; else suggestions.push("Expand — more detail improves quality"); - if (/because|due to|since/i.test(artifact.body)) score++; + if (/because|due to|since/i.test(text)) score++; else suggestions.push("Add rationale"); return { score, maxScore: 2, level: score >= 2 ? "adequate" : "weak", gaps, suggestions }; } @@ -418,12 +425,12 @@ function scoreArtifactQuality( for (const c of criteria) { const ck = c.check.toLowerCase(); let passed = false; - if (ck.includes("non-empty")) passed = artifact.fields.length > 3 || artifact.body.length > 0; - else if (ck.includes("10")) passed = artifact.body.split(/\s+/).length >= 10; - else if (ck.includes("number") || ck.includes("concrete")) passed = /\d/.test(artifact.body); - else if (ck.includes("interpretation") || ck.includes("does not contain")) passed = !/should|better|worse|means|implies/i.test(artifact.body); - else if (ck.includes("prohibition") || ck.includes("requirement")) passed = /must|must not|never|always|shall/i.test(artifact.body); - else passed = artifact.body.split(/\s+/).length >= 5; + if (ck.includes("non-empty")) passed = artifact.fields.length > 3 || text.length > 0; + else if (ck.includes("10")) passed = text.split(/\s+/).length >= 10; + else if (ck.includes("number") || ck.includes("concrete")) passed = /\d/.test(text); + else if (ck.includes("interpretation") || ck.includes("does not contain")) passed = !/should|better|worse|means|implies/i.test(text); + else if (ck.includes("prohibition") || ck.includes("requirement")) passed = /must|must not|never|always|shall/i.test(text); + else passed = text.split(/\s+/).length >= 5; if (passed) score++; else { gaps.push(c.gapMessage); suggestions.push(c.gapMessage); } } @@ -1415,19 +1422,26 @@ async function runEncodeAction( state?: OddkitState, ): Promise { const startMs = Date.now(); - const fullInput = context ? `${input}\n${context}` : input; + // Governance: input generates artifacts; context only informs quality scoring. + // See: klappy://odd/encoding-types/how-to-write-encoding-types#context-vs-input + // Do not pass fullInput to parsers — that would create separate artifacts + // for each context paragraph instead of letting context inform scoring. const types = await discoverEncodingTypes(fetcher, canonUrl); - const structured = isStructuredInput(fullInput); + const structured = isStructuredInput(input); const artifacts = structured - ? parseStructuredInput(fullInput, types) - : parseUnstructuredInput(fullInput, types); + ? parseStructuredInput(input, types) + : parseUnstructuredInput(input, types); - // Score each artifact using its type's quality criteria + // Score each artifact using its type's quality criteria. + // When context is provided, append it to the artifact's body for scoring + // so background information (rationale, alternatives, evidence) counts + // toward the artifact's quality without becoming separate artifacts. const scoredArtifacts = artifacts.map((a) => { const typeDef = types.find((t) => t.letter === a.type); const criteria = typeDef ? typeDef.qualityCriteria : []; - const quality = scoreArtifactQuality(a, criteria); + const scoringText = context ? `${a.body}\n${context}` : undefined; + const quality = scoreArtifactQuality(a, criteria, scoringText); return { title: a.title, type: a.type, typeName: a.typeName, content: a.body, fields: a.fields, quality }; }); From e6e9e6c515419119405d080a1b24dc6052f299bb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 16 Apr 2026 03:06:47 +0000 Subject: [PATCH 2/3] fix: use artifact.body for keyword-pattern criteria to prevent context from corrupting negative/positive checks --- workers/src/orchestrate.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/workers/src/orchestrate.ts b/workers/src/orchestrate.ts index 0954fc6..73961d3 100644 --- a/workers/src/orchestrate.ts +++ b/workers/src/orchestrate.ts @@ -428,8 +428,8 @@ function scoreArtifactQuality( if (ck.includes("non-empty")) passed = artifact.fields.length > 3 || text.length > 0; else if (ck.includes("10")) passed = text.split(/\s+/).length >= 10; else if (ck.includes("number") || ck.includes("concrete")) passed = /\d/.test(text); - else if (ck.includes("interpretation") || ck.includes("does not contain")) passed = !/should|better|worse|means|implies/i.test(text); - else if (ck.includes("prohibition") || ck.includes("requirement")) passed = /must|must not|never|always|shall/i.test(text); + else if (ck.includes("interpretation") || ck.includes("does not contain")) passed = !/should|better|worse|means|implies/i.test(artifact.body); + else if (ck.includes("prohibition") || ck.includes("requirement")) passed = /must|must not|never|always|shall/i.test(artifact.body); else passed = text.split(/\s+/).length >= 5; if (passed) score++; else { gaps.push(c.gapMessage); suggestions.push(c.gapMessage); } From e95af368db445a5127bf769d14760d6734f29ead Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 16 Apr 2026 03:11:10 +0000 Subject: [PATCH 3/3] Fix non-empty criterion check to use artifact.body instead of text The non-empty check in scoreArtifactQuality was using text (which includes appended context) instead of artifact.body. This meant an artifact with an empty body would pass the non-empty check whenever context was supplied, defeating the purpose of the validity check. Restored the check to use artifact.body.length so context informs quality scoring but cannot substitute for actual artifact content. --- workers/src/orchestrate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workers/src/orchestrate.ts b/workers/src/orchestrate.ts index 73961d3..a1a43ea 100644 --- a/workers/src/orchestrate.ts +++ b/workers/src/orchestrate.ts @@ -425,7 +425,7 @@ function scoreArtifactQuality( for (const c of criteria) { const ck = c.check.toLowerCase(); let passed = false; - if (ck.includes("non-empty")) passed = artifact.fields.length > 3 || text.length > 0; + if (ck.includes("non-empty")) passed = artifact.fields.length > 3 || artifact.body.length > 0; else if (ck.includes("10")) passed = text.split(/\s+/).length >= 10; else if (ck.includes("number") || ck.includes("concrete")) passed = /\d/.test(text); else if (ck.includes("interpretation") || ck.includes("does not contain")) passed = !/should|better|worse|means|implies/i.test(artifact.body);