@markstanden/opencode-test-container 0.0.0-stage → 0.1.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.
Files changed (121) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +263 -2
  3. package/container/Containerfile +24 -0
  4. package/container/README.md +29 -0
  5. package/dist/adapters/opencode/events.d.ts +90 -0
  6. package/dist/adapters/opencode/events.js +213 -0
  7. package/dist/adapters/opencode/runtime.d.ts +87 -0
  8. package/dist/adapters/opencode/runtime.js +136 -0
  9. package/dist/adapters/podman/container.d.ts +62 -0
  10. package/dist/adapters/podman/container.js +119 -0
  11. package/dist/adapters/podman/image.d.ts +50 -0
  12. package/dist/adapters/podman/image.js +124 -0
  13. package/dist/adapters/process.d.ts +38 -0
  14. package/dist/adapters/process.js +71 -0
  15. package/dist/campaign/assess.d.ts +45 -0
  16. package/dist/campaign/assess.js +208 -0
  17. package/dist/campaign/close.d.ts +17 -0
  18. package/dist/campaign/close.js +112 -0
  19. package/dist/campaign/evidence-guard.d.ts +24 -0
  20. package/dist/campaign/evidence-guard.js +49 -0
  21. package/dist/campaign/freeze.d.ts +94 -0
  22. package/dist/campaign/freeze.js +297 -0
  23. package/dist/campaign/inventory.d.ts +62 -0
  24. package/dist/campaign/inventory.js +192 -0
  25. package/dist/campaign/judge.d.ts +209 -0
  26. package/dist/campaign/judge.js +1285 -0
  27. package/dist/campaign/ledger.d.ts +64 -0
  28. package/dist/campaign/ledger.js +149 -0
  29. package/dist/campaign/prepare.d.ts +129 -0
  30. package/dist/campaign/prepare.js +483 -0
  31. package/dist/campaign/primary.d.ts +96 -0
  32. package/dist/campaign/primary.js +189 -0
  33. package/dist/campaign/run.d.ts +151 -0
  34. package/dist/campaign/run.js +562 -0
  35. package/dist/campaign/status.d.ts +76 -0
  36. package/dist/campaign/status.js +237 -0
  37. package/dist/cli/otc.d.ts +51 -0
  38. package/dist/cli/otc.js +435 -0
  39. package/dist/contracts/canonical.d.ts +21 -0
  40. package/dist/contracts/canonical.js +44 -0
  41. package/dist/contracts/types.d.ts +261 -0
  42. package/dist/contracts/types.js +9 -0
  43. package/dist/contracts/validate.d.ts +72 -0
  44. package/dist/contracts/validate.js +1021 -0
  45. package/dist/contracts/version.d.ts +44 -0
  46. package/dist/contracts/version.js +147 -0
  47. package/dist/evaluators/docs/analysis/accounting.d.ts +140 -0
  48. package/dist/evaluators/docs/analysis/accounting.js +237 -0
  49. package/dist/evaluators/docs/analysis/blinding.d.ts +40 -0
  50. package/dist/evaluators/docs/analysis/blinding.js +80 -0
  51. package/dist/evaluators/docs/analysis/bounds.d.ts +40 -0
  52. package/dist/evaluators/docs/analysis/bounds.js +31 -0
  53. package/dist/evaluators/docs/analysis/budget.d.ts +137 -0
  54. package/dist/evaluators/docs/analysis/budget.js +305 -0
  55. package/dist/evaluators/docs/analysis/closure.d.ts +175 -0
  56. package/dist/evaluators/docs/analysis/closure.js +251 -0
  57. package/dist/evaluators/docs/analysis/decision.d.ts +59 -0
  58. package/dist/evaluators/docs/analysis/decision.js +161 -0
  59. package/dist/evaluators/docs/analysis/freeze.d.ts +201 -0
  60. package/dist/evaluators/docs/analysis/freeze.js +395 -0
  61. package/dist/evaluators/docs/analysis/guardrails.d.ts +51 -0
  62. package/dist/evaluators/docs/analysis/guardrails.js +194 -0
  63. package/dist/evaluators/docs/analysis/journal.d.ts +150 -0
  64. package/dist/evaluators/docs/analysis/journal.js +475 -0
  65. package/dist/evaluators/docs/analysis/ledger.d.ts +78 -0
  66. package/dist/evaluators/docs/analysis/ledger.js +145 -0
  67. package/dist/evaluators/docs/analysis/measures.d.ts +302 -0
  68. package/dist/evaluators/docs/analysis/measures.js +1069 -0
  69. package/dist/evaluators/docs/analysis/model.d.ts +236 -0
  70. package/dist/evaluators/docs/analysis/model.js +740 -0
  71. package/dist/evaluators/docs/analysis/pairing.d.ts +68 -0
  72. package/dist/evaluators/docs/analysis/pairing.js +95 -0
  73. package/dist/evaluators/docs/analysis/records.d.ts +229 -0
  74. package/dist/evaluators/docs/analysis/records.js +935 -0
  75. package/dist/evaluators/docs/analysis/recovery.d.ts +225 -0
  76. package/dist/evaluators/docs/analysis/recovery.js +507 -0
  77. package/dist/evaluators/docs/analysis/render.d.ts +235 -0
  78. package/dist/evaluators/docs/analysis/render.js +566 -0
  79. package/dist/evaluators/docs/analysis/replay.d.ts +15 -0
  80. package/dist/evaluators/docs/analysis/replay.js +1272 -0
  81. package/dist/evaluators/docs/analysis/screening/inventory.d.ts +136 -0
  82. package/dist/evaluators/docs/analysis/screening/inventory.js +307 -0
  83. package/dist/evaluators/docs/analysis/screening/noteVersion.d.ts +15 -0
  84. package/dist/evaluators/docs/analysis/screening/noteVersion.js +46 -0
  85. package/dist/evaluators/docs/analysis/screening/reconcile.d.ts +38 -0
  86. package/dist/evaluators/docs/analysis/screening/reconcile.js +40 -0
  87. package/dist/evaluators/docs/analysis/screening/rules.d.ts +163 -0
  88. package/dist/evaluators/docs/analysis/screening/rules.js +330 -0
  89. package/dist/evaluators/docs/analysis/screening/scan.d.ts +72 -0
  90. package/dist/evaluators/docs/analysis/screening/scan.js +362 -0
  91. package/dist/evaluators/docs/analysis/selector.d.ts +68 -0
  92. package/dist/evaluators/docs/analysis/selector.js +123 -0
  93. package/dist/evaluators/docs/assess.d.ts +129 -0
  94. package/dist/evaluators/docs/assess.js +433 -0
  95. package/dist/evaluators/docs/campaign.d.ts +133 -0
  96. package/dist/evaluators/docs/campaign.js +222 -0
  97. package/dist/evaluators/docs/index.d.ts +25 -0
  98. package/dist/evaluators/docs/index.js +25 -0
  99. package/dist/evaluators/docs/journal.d.ts +150 -0
  100. package/dist/evaluators/docs/journal.js +475 -0
  101. package/dist/evaluators/docs/scoring.d.ts +367 -0
  102. package/dist/evaluators/docs/scoring.js +662 -0
  103. package/dist/evaluators/skills/assess.d.ts +94 -0
  104. package/dist/evaluators/skills/assess.js +207 -0
  105. package/dist/index.d.ts +46 -0
  106. package/dist/index.js +29 -0
  107. package/dist/legacy/prod014.d.ts +86 -0
  108. package/dist/legacy/prod014.js +209 -0
  109. package/dist/reporting/report.d.ts +79 -0
  110. package/dist/reporting/report.js +242 -0
  111. package/docs/architecture.md +106 -0
  112. package/docs/capability-matrix.md +84 -0
  113. package/docs/offline-rehearsal.md +75 -0
  114. package/docs/parity-record.md +81 -0
  115. package/docs/pr-description.md +103 -0
  116. package/docs/schema.md +97 -0
  117. package/examples/skills/README.md +13 -0
  118. package/examples/skills/campaign.json +73 -0
  119. package/examples/skills/skills/otel-helper/SKILL.md +10 -0
  120. package/examples/skills/suite.json +29 -0
  121. package/package.json +45 -5
@@ -0,0 +1,163 @@
1
+ import type { DocumentationChange, LakeDocument } from "../../campaign.ts";
2
+ /** The rules version this module implements. */
3
+ export declare const SCREENING_RULES_VERSION = "v1";
4
+ /** Where a finding's excerpt came from. */
5
+ export type SourceKind = "resulting-document" | "before-document" | "after-document" | "final-response" | "task-prompt" | "recorded-write";
6
+ /** The finding categories v1 implements. */
7
+ export type FindingCategory = "structure" | "claim-contradiction" | "permission-visibility" | "duplication" | "transient-wording" | "fact-negation" | "fact-obsolete-current" | "fact-conflict" | "fact-coverage-unknown";
8
+ /** Whether a finding is an observed fact or an advisory signal. */
9
+ export type RuleAuthority = "mechanical" | "advisory";
10
+ /** Whether a visual restriction was visible to the model, hidden, or unknown. */
11
+ export type FindingVisibility = "visible" | "hidden" | "unknown";
12
+ /** Where a finding's evidence sits in the recorded trial. */
13
+ export type EvidenceRole = "before" | "after" | "response" | "prompt" | "writes";
14
+ /** The declared identity of the trial a finding came from. */
15
+ export interface TrialIdentity {
16
+ /** The trial stamp. */
17
+ stamp: string;
18
+ /** The arm the trial ran. */
19
+ arm: string;
20
+ /** The scenario the trial ran. */
21
+ scenario: string;
22
+ }
23
+ /** The hashed source one finding cites. */
24
+ export interface FindingSource {
25
+ /** The source kind. */
26
+ kind: SourceKind;
27
+ /** The source's stable reference (root-relative path or label). */
28
+ reference: string;
29
+ /** SHA-256 of the source's exact text. */
30
+ sha256: string;
31
+ }
32
+ /** One static-screening finding before its trial provenance is attached. */
33
+ export interface FindingDraft {
34
+ /** The rule that produced the finding. */
35
+ ruleId: string;
36
+ /** The rule's version. */
37
+ ruleVersion: string;
38
+ /** The finding category. */
39
+ category: FindingCategory;
40
+ /** The scenario the rule was applied to, or `any`. */
41
+ applicability: string;
42
+ /** Whether the finding is mechanical or advisory. */
43
+ authority: RuleAuthority;
44
+ /** The hashed source. */
45
+ source: FindingSource;
46
+ /** The exact excerpt the finding cites. */
47
+ excerpt: string;
48
+ /** The excerpt's span within the source. */
49
+ span: {
50
+ start: number;
51
+ end: number;
52
+ };
53
+ /** The inventory fact id, for inventory rules. */
54
+ factId?: string | undefined;
55
+ /** The recorded visibility, for permission findings. */
56
+ visibility?: FindingVisibility | undefined;
57
+ /** Why the excerpt triggers this rule, including the limits of the signal. */
58
+ explanation: string;
59
+ }
60
+ /** One static-screening finding with its trial provenance. */
61
+ export interface Finding extends FindingDraft {
62
+ /** The trial the finding came from. */
63
+ trial: TrialIdentity;
64
+ /** The evidence role the finding cites. */
65
+ role: EvidenceRole;
66
+ }
67
+ /**
68
+ * SHA-256 of one source's exact text.
69
+ * @param text The source text.
70
+ * @returns The lowercase hex digest.
71
+ */
72
+ export declare function sha256Of(text: string): string;
73
+ /**
74
+ * Build a hashed source record.
75
+ * @param kind The source kind.
76
+ * @param reference The source reference.
77
+ * @param text The source text.
78
+ * @returns The source record.
79
+ */
80
+ export declare function sourceOf(kind: SourceKind, reference: string, text: string): FindingSource;
81
+ /**
82
+ * Structural findings for one resulting document: an H1 title and a claim
83
+ * section are required by the documentation convention.
84
+ * @param document The resulting document.
85
+ * @param scenario The scenario id.
86
+ * @returns Findings, each citing the first nonblank line.
87
+ */
88
+ export declare function structureFindings(document: LakeDocument, scenario: string): FindingDraft[];
89
+ /**
90
+ * Contradictions between a response's claimed documentation outcome and the
91
+ * observed document mutations.
92
+ * @param input The answer text, mutation facts and source metadata.
93
+ * @param input.answer The final response text.
94
+ * @param input.changes The recorded document changes.
95
+ * @param input.reference The answer's stable reference.
96
+ * @param input.scenario The scenario id.
97
+ * @returns One finding per contradicting claim, with the claim span.
98
+ */
99
+ export declare function claimMutationFindings(input: {
100
+ answer: string;
101
+ changes: DocumentationChange;
102
+ reference: string;
103
+ scenario: string;
104
+ }): FindingDraft[];
105
+ /**
106
+ * Repetition signal: content repeated from an existing document into a new or
107
+ * modified one. Exact/normalised repetition is advisory — it does not prove
108
+ * the repetition is unnecessary.
109
+ * @param beforeDocs The documents before the trial.
110
+ * @param afterDocs The documents after the trial.
111
+ * @param scenario The scenario id.
112
+ * @returns One finding per changed document whose content repeats existing text.
113
+ */
114
+ export declare function duplicationFindings(beforeDocs: readonly LakeDocument[], afterDocs: readonly LakeDocument[], scenario: string): FindingDraft[];
115
+ /**
116
+ * Transient-state wording signal, with durable-troubleshooting counterexamples
117
+ * suppressing the finding for the sentence.
118
+ * @param input The text to scan and its source metadata.
119
+ * @param input.text The text to scan.
120
+ * @param input.reference The source reference.
121
+ * @param input.kind The source kind.
122
+ * @param input.scenario The scenario id.
123
+ * @param input.patterns Configurable transient-wording regex sources.
124
+ * @param input.counterexamples Durable-troubleshooting regex sources.
125
+ * @returns One advisory finding per non-counterexample match.
126
+ */
127
+ export declare function transientWordingFindings(input: {
128
+ text: string;
129
+ reference: string;
130
+ kind: SourceKind;
131
+ scenario: string;
132
+ patterns: readonly string[];
133
+ counterexamples: readonly string[];
134
+ }): FindingDraft[];
135
+ /**
136
+ * Whether one path matches one allowed glob.
137
+ * @param pattern The allowed glob.
138
+ * @param path The changed path.
139
+ * @returns True when the path is inside the allowed set.
140
+ */
141
+ export declare function globMatches(pattern: string, path: string): boolean;
142
+ /**
143
+ * Mechanical permission findings: a changed path outside the recorded allowed
144
+ * set, classified by whether the task communicated the restriction.
145
+ * @param input The changed paths, allowed globs, prompt and writes text.
146
+ * @param input.changedPaths Paths the trial changed.
147
+ * @param input.allowedGlobs The recorded allowed globs; empty means no policy.
148
+ * @param input.taskPrompt The exact task prompt, or null when unavailable.
149
+ * @param input.visibleConstraints The constraints recorded as visible, if any.
150
+ * @param input.writesText The recorded writes report text.
151
+ * @param input.reference The writes report reference.
152
+ * @param input.scenario The scenario id.
153
+ * @returns One finding per out-of-policy path.
154
+ */
155
+ export declare function permissionFindings(input: {
156
+ changedPaths: readonly string[];
157
+ allowedGlobs: readonly string[];
158
+ taskPrompt: string | null;
159
+ visibleConstraints?: readonly string[] | undefined;
160
+ writesText: string;
161
+ reference: string;
162
+ scenario: string;
163
+ }): FindingDraft[];
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Static-screening rules over recorded trial evidence.
3
+ *
4
+ * Every finding carries its rule identity and version, the source kind and
5
+ * hash, the exact excerpt and span, and its authority: a mechanical fact
6
+ * (both the claim text and the observed state are recorded) or an advisory
7
+ * signal (a wording pattern that needs a reviewer). A finding never changes
8
+ * eligibility, an outcome, a score or a guardrail by itself.
9
+ */
10
+ import { createHash } from "node:crypto";
11
+ /** The rules version this module implements. */
12
+ export const SCREENING_RULES_VERSION = "v1";
13
+ /** A response assertion of having authored documentation. */
14
+ const WRITE_CLAIM = /\b(?:created|wrote|added|updated|replaced|recorded)\b[^\n.]{0,80}\b(?:document|doc|note|entry|page|knowledge[- ]base|markdown)\b/i;
15
+ /** A response assertion of having written nothing. */
16
+ const NO_WRITE_CLAIM = /\b(?:did not|didn't|never|no)\b[^\n.]{0,40}\b(?:write|create|update|change|edit|record|document)\b/i;
17
+ /**
18
+ * SHA-256 of one source's exact text.
19
+ * @param text The source text.
20
+ * @returns The lowercase hex digest.
21
+ */
22
+ export function sha256Of(text) {
23
+ return createHash("sha256").update(text, "utf8").digest("hex");
24
+ }
25
+ /**
26
+ * Build a hashed source record.
27
+ * @param kind The source kind.
28
+ * @param reference The source reference.
29
+ * @param text The source text.
30
+ * @returns The source record.
31
+ */
32
+ export function sourceOf(kind, reference, text) {
33
+ return { kind, reference, sha256: sha256Of(text) };
34
+ }
35
+ /**
36
+ * Structural findings for one resulting document: an H1 title and a claim
37
+ * section are required by the documentation convention.
38
+ * @param document The resulting document.
39
+ * @param scenario The scenario id.
40
+ * @returns Findings, each citing the first nonblank line.
41
+ */
42
+ export function structureFindings(document, scenario) {
43
+ const source = sourceOf("resulting-document", document.path, document.contents);
44
+ const firstLine = document.contents
45
+ .split("\n")
46
+ .map((line) => line.trim())
47
+ .find((line) => line !== "");
48
+ const excerpt = firstLine ?? "";
49
+ const span = { start: 0, end: excerpt.length };
50
+ const findings = [];
51
+ if (!/^#\s+\S/m.test(document.contents)) {
52
+ findings.push({
53
+ ruleId: "document-structure",
54
+ ruleVersion: SCREENING_RULES_VERSION,
55
+ category: "structure",
56
+ applicability: scenario,
57
+ authority: "mechanical",
58
+ source,
59
+ excerpt,
60
+ span,
61
+ explanation: "the document has no H1 title",
62
+ });
63
+ }
64
+ if (!/^#{2,3}\s+\S/m.test(document.contents)) {
65
+ findings.push({
66
+ ruleId: "document-structure",
67
+ ruleVersion: SCREENING_RULES_VERSION,
68
+ category: "structure",
69
+ applicability: scenario,
70
+ authority: "mechanical",
71
+ source,
72
+ excerpt,
73
+ span,
74
+ explanation: "the document has no claim section heading",
75
+ });
76
+ }
77
+ return findings;
78
+ }
79
+ /**
80
+ * Contradictions between a response's claimed documentation outcome and the
81
+ * observed document mutations.
82
+ * @param input The answer text, mutation facts and source metadata.
83
+ * @param input.answer The final response text.
84
+ * @param input.changes The recorded document changes.
85
+ * @param input.reference The answer's stable reference.
86
+ * @param input.scenario The scenario id.
87
+ * @returns One finding per contradicting claim, with the claim span.
88
+ */
89
+ export function claimMutationFindings(input) {
90
+ const source = sourceOf("final-response", input.reference, input.answer);
91
+ const findings = [];
92
+ const claimedWrite = WRITE_CLAIM.exec(input.answer);
93
+ if (claimedWrite !== null && !input.changes.documentsChanged) {
94
+ findings.push({
95
+ ruleId: "claim-mutation-contradiction",
96
+ ruleVersion: SCREENING_RULES_VERSION,
97
+ category: "claim-contradiction",
98
+ applicability: input.scenario,
99
+ authority: "mechanical",
100
+ source,
101
+ excerpt: claimedWrite[0],
102
+ span: {
103
+ start: claimedWrite.index,
104
+ end: claimedWrite.index + claimedWrite[0].length,
105
+ },
106
+ explanation: "the response claims a documentation write; the recorded state shows no document changed",
107
+ });
108
+ }
109
+ const claimedNone = NO_WRITE_CLAIM.exec(input.answer);
110
+ if (claimedNone !== null && input.changes.documentsChanged) {
111
+ findings.push({
112
+ ruleId: "claim-mutation-contradiction",
113
+ ruleVersion: SCREENING_RULES_VERSION,
114
+ category: "claim-contradiction",
115
+ applicability: input.scenario,
116
+ authority: "mechanical",
117
+ source,
118
+ excerpt: claimedNone[0],
119
+ span: {
120
+ start: claimedNone.index,
121
+ end: claimedNone.index + claimedNone[0].length,
122
+ },
123
+ explanation: "the response claims no documentation change; the recorded state shows changed documents",
124
+ });
125
+ }
126
+ return findings;
127
+ }
128
+ /** Whether a document is new or modified between the two states. */
129
+ function isChanged(document, before) {
130
+ return before.get(document.path) !== document.contents;
131
+ }
132
+ /**
133
+ * Normalise one line for repetition comparison.
134
+ * @param line The raw line.
135
+ * @returns The comparable form; short or heading lines return an empty string.
136
+ */
137
+ function normalisedLine(line) {
138
+ const trimmed = line.trim();
139
+ if (trimmed.length < 24 || /^#{1,6}\s/.test(trimmed))
140
+ return "";
141
+ return trimmed.replace(/\s+/g, " ").toLowerCase();
142
+ }
143
+ /**
144
+ * Repetition signal: content repeated from an existing document into a new or
145
+ * modified one. Exact/normalised repetition is advisory — it does not prove
146
+ * the repetition is unnecessary.
147
+ * @param beforeDocs The documents before the trial.
148
+ * @param afterDocs The documents after the trial.
149
+ * @param scenario The scenario id.
150
+ * @returns One finding per changed document whose content repeats existing text.
151
+ */
152
+ export function duplicationFindings(beforeDocs, afterDocs, scenario) {
153
+ const before = new Map(beforeDocs.map((doc) => [doc.path, doc.contents]));
154
+ const existing = new Set(beforeDocs.flatMap((doc) => doc.contents.split("\n").map(normalisedLine).filter(Boolean)));
155
+ const findings = [];
156
+ for (const document of afterDocs) {
157
+ if (!isChanged(document, before))
158
+ continue;
159
+ const source = sourceOf("resulting-document", document.path, document.contents);
160
+ let offset = 0;
161
+ for (const line of document.contents.split("\n")) {
162
+ const key = normalisedLine(line);
163
+ if (key !== "" && existing.has(key)) {
164
+ const trimmed = line.trim();
165
+ const start = offset + line.indexOf(trimmed);
166
+ findings.push({
167
+ ruleId: "duplication-signal",
168
+ ruleVersion: SCREENING_RULES_VERSION,
169
+ category: "duplication",
170
+ applicability: scenario,
171
+ authority: "advisory",
172
+ source,
173
+ excerpt: trimmed,
174
+ span: { start, end: start + trimmed.length },
175
+ explanation: "this line repeats content already present before the trial; repetition may still be a legitimate update",
176
+ });
177
+ break;
178
+ }
179
+ offset += line.length + 1;
180
+ }
181
+ }
182
+ return findings;
183
+ }
184
+ /**
185
+ * Transient-state wording signal, with durable-troubleshooting counterexamples
186
+ * suppressing the finding for the sentence.
187
+ * @param input The text to scan and its source metadata.
188
+ * @param input.text The text to scan.
189
+ * @param input.reference The source reference.
190
+ * @param input.kind The source kind.
191
+ * @param input.scenario The scenario id.
192
+ * @param input.patterns Configurable transient-wording regex sources.
193
+ * @param input.counterexamples Durable-troubleshooting regex sources.
194
+ * @returns One advisory finding per non-counterexample match.
195
+ */
196
+ export function transientWordingFindings(input) {
197
+ const source = sourceOf(input.kind, input.reference, input.text);
198
+ const findings = [];
199
+ for (const pattern of input.patterns) {
200
+ const match = new RegExp(pattern, "i").exec(input.text);
201
+ if (match === null)
202
+ continue;
203
+ const sentence = sentenceAround(input.text, match.index);
204
+ const durable = input.counterexamples.some((counter) => new RegExp(counter, "i").test(sentence.text));
205
+ if (durable)
206
+ continue;
207
+ findings.push({
208
+ ruleId: "transient-wording",
209
+ ruleVersion: SCREENING_RULES_VERSION,
210
+ category: "transient-wording",
211
+ applicability: input.scenario,
212
+ authority: "advisory",
213
+ source,
214
+ excerpt: match[0],
215
+ span: {
216
+ start: match.index,
217
+ end: match.index + match[0].length,
218
+ },
219
+ explanation: "possible transient session-state wording with no durable-troubleshooting cue; a reviewer decides whether it has future value",
220
+ });
221
+ }
222
+ return findings;
223
+ }
224
+ /**
225
+ * The sentence containing one offset.
226
+ * @param text The text.
227
+ * @param index The offset inside the sentence.
228
+ * @returns The sentence text and its span.
229
+ */
230
+ function sentenceAround(text, index) {
231
+ const before = text.lastIndexOf("\n", index);
232
+ const after = text.indexOf("\n", index);
233
+ const start = before === -1 ? 0 : before + 1;
234
+ const end = after === -1 ? text.length : after;
235
+ return { text: text.slice(start, end), start, end };
236
+ }
237
+ /**
238
+ * Convert one glob pattern to a safe regular expression source.
239
+ * @param pattern The glob, with `**` crossing directories and `*`/`?` inside one.
240
+ * @returns An anchored regex source.
241
+ */
242
+ function globSource(pattern) {
243
+ const segments = pattern.split("/").map((segment) => {
244
+ if (segment === "**")
245
+ return ".*";
246
+ return segment
247
+ .split("*")
248
+ .map((part) => part.replace(/[.+^${}()|[\]\\?]/g, "\\$&"))
249
+ .join("[^/]*")
250
+ .replace(/\\\?/g, "[^/]");
251
+ });
252
+ return `^${segments.join("/")}$`;
253
+ }
254
+ /**
255
+ * Whether one path matches one allowed glob.
256
+ * @param pattern The allowed glob.
257
+ * @param path The changed path.
258
+ * @returns True when the path is inside the allowed set.
259
+ */
260
+ export function globMatches(pattern, path) {
261
+ return new RegExp(globSource(pattern)).test(path);
262
+ }
263
+ /**
264
+ * Whether the task communicated a restriction on one changed path. The
265
+ * recorded visible constraints are authoritative when present; otherwise the
266
+ * prompt text is the only visibility source. Without a prompt the visibility
267
+ * stays unknown rather than being asserted either way.
268
+ * @param path The changed path.
269
+ * @param taskPrompt The exact task prompt, or null when unavailable.
270
+ * @param visibleConstraints The constraints the campaign recorded as visible.
271
+ * @returns The visibility classification.
272
+ */
273
+ function visibilityOf(path, taskPrompt, visibleConstraints) {
274
+ if (taskPrompt === null)
275
+ return "unknown";
276
+ const baseline = path.split("/").pop() ?? path;
277
+ if (visibleConstraints !== undefined) {
278
+ return visibleConstraints.some((constraint) => constraint.includes(baseline))
279
+ ? "visible"
280
+ : "hidden";
281
+ }
282
+ return taskPrompt.includes(baseline) ? "visible" : "hidden";
283
+ }
284
+ /**
285
+ * Mechanical permission findings: a changed path outside the recorded allowed
286
+ * set, classified by whether the task communicated the restriction.
287
+ * @param input The changed paths, allowed globs, prompt and writes text.
288
+ * @param input.changedPaths Paths the trial changed.
289
+ * @param input.allowedGlobs The recorded allowed globs; empty means no policy.
290
+ * @param input.taskPrompt The exact task prompt, or null when unavailable.
291
+ * @param input.visibleConstraints The constraints recorded as visible, if any.
292
+ * @param input.writesText The recorded writes report text.
293
+ * @param input.reference The writes report reference.
294
+ * @param input.scenario The scenario id.
295
+ * @returns One finding per out-of-policy path.
296
+ */
297
+ export function permissionFindings(input) {
298
+ if (input.allowedGlobs.length === 0)
299
+ return [];
300
+ const source = sourceOf("recorded-write", input.reference, input.writesText);
301
+ const findings = [];
302
+ for (const path of input.changedPaths) {
303
+ if (input.allowedGlobs.some((glob) => globMatches(glob, path))) {
304
+ continue;
305
+ }
306
+ const baseline = path.split("/").pop() ?? path;
307
+ const visibility = visibilityOf(path, input.taskPrompt, input.visibleConstraints);
308
+ const start = input.writesText.indexOf(path);
309
+ findings.push({
310
+ ruleId: "permission-visibility",
311
+ ruleVersion: SCREENING_RULES_VERSION,
312
+ category: "permission-visibility",
313
+ applicability: input.scenario,
314
+ authority: "mechanical",
315
+ source,
316
+ excerpt: path,
317
+ span: {
318
+ start: start === -1 ? 0 : start,
319
+ end: start === -1 ? path.length : start + path.length,
320
+ },
321
+ visibility,
322
+ explanation: visibility === "unknown"
323
+ ? `hidden-policy mismatch cannot be classified: the recorded policy forbids ${path} and no task prompt was available to establish visibility`
324
+ : visibility === "visible"
325
+ ? `the task communicated the restriction on ${baseline}; the change is a visible-policy breach`
326
+ : `hidden-policy mismatch: the recorded policy forbids ${path} but the task never exposed that restriction`,
327
+ });
328
+ }
329
+ return findings;
330
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Static-screening orchestration for one recorded trial.
3
+ *
4
+ * Reads the resulting documents, before/after states, the final response, the
5
+ * task prompt and the recorded writes, applies the configured rules and
6
+ * returns findings with hashed, root-relative sources and trial provenance.
7
+ * Every input is a {@link Loaded} record: a rule runs only with sufficient
8
+ * evidence, so an unobserved change can never be published as a mechanical
9
+ * contradiction. The screen is advisory infrastructure: it never modifies
10
+ * judge inputs, eligibility, outcomes, scores or guardrails, and it is
11
+ * generated independently of any judge packet.
12
+ */
13
+ import { type DocumentationChange, type LakeDocument } from "../../campaign.ts";
14
+ import type { MutationPolicySpec, PreservationInventorySpec } from "../model.ts";
15
+ import type { Loaded } from "../records.ts";
16
+ import { type Finding, type FindingSource, type TrialIdentity } from "./rules.ts";
17
+ /** The root-relative references a screen cites. */
18
+ export interface ScreenPaths {
19
+ /** The trial's prompt file. */
20
+ prompt: string;
21
+ /** The trial's final answer file. */
22
+ finalAnswer: string;
23
+ /** The trial's writes report. */
24
+ writes: string;
25
+ /** The before-trial lake document root. */
26
+ beforeRoot: string;
27
+ /** The after-trial lake document root. */
28
+ afterRoot: string;
29
+ }
30
+ /** Everything one trial's static screen reads. */
31
+ export interface ScreenTrialInput {
32
+ /** The trial identity this screen belongs to. */
33
+ trial: TrialIdentity;
34
+ /** The exact task prompt. */
35
+ taskPrompt: Loaded<string>;
36
+ /** The trial's final answer. */
37
+ finalAnswer: Loaded<string>;
38
+ /** The knowledge-lake documents before the trial. */
39
+ beforeDocs: Loaded<LakeDocument[]>;
40
+ /** The knowledge-lake documents after the trial. */
41
+ afterDocs: Loaded<LakeDocument[]>;
42
+ /** The recorded writes report text. */
43
+ writesText: Loaded<string>;
44
+ /** The root-relative references for the cited evidence. */
45
+ paths: ScreenPaths;
46
+ /** The scenario's configured preservation inventory, when present. */
47
+ inventory?: PreservationInventorySpec | undefined;
48
+ /** Configured transient-wording regex sources. */
49
+ transientPatterns?: string[] | undefined;
50
+ /** Durable-troubleshooting counterexample regex sources. */
51
+ durableCounterexamples?: string[] | undefined;
52
+ /** The scenario's recorded mutation policy, when the campaign declares one. */
53
+ mutationPolicy?: MutationPolicySpec | undefined;
54
+ }
55
+ /** The screen result for one trial. */
56
+ export interface ScreenResult {
57
+ /** Findings, stably sorted by source, span and rule. */
58
+ findings: Finding[];
59
+ /** Every hashed source the screen read, sorted by reference. */
60
+ sources: FindingSource[];
61
+ /** The recorded document change classification, or null when unobservable. */
62
+ changes: DocumentationChange | null;
63
+ }
64
+ /**
65
+ * Screen one recorded trial. Each rule sees only the evidence it needs: the
66
+ * after state for resulting-document rules, both states for mutation and
67
+ * duplication, the answer for response wording, and the writes plus a
68
+ * recorded policy for permissions.
69
+ * @param input Everything the screen reads.
70
+ * @returns Findings with provenance, hashed sources and the change state.
71
+ */
72
+ export declare function screenTrial(input: ScreenTrialInput): ScreenResult;