project-tiny-context-harness 0.2.69 → 0.2.71

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.
@@ -0,0 +1,196 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { pathExists, readText } from "./fs.js";
4
+ const PATH_PATTERN = /(?:`([^`]+)`)|((?:\.\/)?(?:project_context|packages|tests|tmp|src|app|pages|components|\.codex|tools|examples)\/[A-Za-z0-9._@+/-]+(?:\.(?:md|ts|tsx|js|mjs|json|yaml|yml|toml|png|jpe?g|webp|html|css|scss|py|go|rs|java|cs|sh|ps1))?)/g;
5
+ const WEAK_PROOF_PATTERNS = [
6
+ /\bchecked path(?:s)?\b/i,
7
+ /\bsampled(?:[_ -]?only)?\b/i,
8
+ /\bnot live-proven\b/i,
9
+ /\blacks?\b/i,
10
+ /\bmissing\b/i,
11
+ /\bgap\b/i,
12
+ /\bpartial\b/i,
13
+ /\bblocked\b/i,
14
+ /\bunconfigured\b/i,
15
+ /\bnot visible by default\b/i
16
+ ];
17
+ export async function resolveInputFile(projectRoot, value, defaultPath) {
18
+ const raw = value ?? defaultPath;
19
+ const absolute = path.isAbsolute(raw) ? path.resolve(raw) : path.resolve(projectRoot, raw);
20
+ let candidate = absolute;
21
+ try {
22
+ const stat = await fs.stat(absolute);
23
+ if (stat.isDirectory()) {
24
+ candidate = path.join(absolute, defaultPath);
25
+ }
26
+ }
27
+ catch {
28
+ if (!path.extname(absolute)) {
29
+ candidate = path.join(absolute, defaultPath);
30
+ }
31
+ }
32
+ return candidate;
33
+ }
34
+ export async function resolveInputDir(projectRoot, value, defaultPath) {
35
+ return path.isAbsolute(value ?? defaultPath)
36
+ ? path.resolve(value ?? defaultPath)
37
+ : path.resolve(projectRoot, value ?? defaultPath);
38
+ }
39
+ export function repoRelative(projectRoot, file) {
40
+ return path.relative(projectRoot, file).split(path.sep).join("/");
41
+ }
42
+ export async function readRequiredFile(projectRoot, file, label, errors) {
43
+ if (!isInside(projectRoot, file)) {
44
+ errors.push(`${label} must stay inside the project root: ${file}`);
45
+ return undefined;
46
+ }
47
+ if (!(await pathExists(file))) {
48
+ errors.push(`${label} is missing: ${repoRelative(projectRoot, file)}`);
49
+ return undefined;
50
+ }
51
+ return readText(file);
52
+ }
53
+ export function parseRequiredTable(content, heading, expectedHeaders, label, errors) {
54
+ const section = sectionBody(content, heading);
55
+ if (section === undefined) {
56
+ errors.push(`plan contract is missing section: ${heading}`);
57
+ return undefined;
58
+ }
59
+ const lines = section.split(/\r?\n/);
60
+ for (let index = 0; index < lines.length; index += 1) {
61
+ if (!lines[index].includes("|")) {
62
+ continue;
63
+ }
64
+ const headers = splitTableLine(lines[index]);
65
+ const normalized = headers.map(normalizeHeader);
66
+ if (!expectedHeaders.every((header) => normalized.includes(normalizeHeader(header)))) {
67
+ continue;
68
+ }
69
+ const rows = [];
70
+ for (let rowIndex = index + 1; rowIndex < lines.length; rowIndex += 1) {
71
+ const line = lines[rowIndex];
72
+ if (!line.includes("|")) {
73
+ break;
74
+ }
75
+ if (isSeparatorLine(line)) {
76
+ continue;
77
+ }
78
+ const values = splitTableLine(line);
79
+ const cells = {};
80
+ for (const [cellIndex, header] of normalized.entries()) {
81
+ cells[header] = values[cellIndex]?.trim() ?? "";
82
+ }
83
+ rows.push({
84
+ index: rows.length + 1,
85
+ line: rowIndex + 1,
86
+ cells,
87
+ text: Object.values(cells).join(" ")
88
+ });
89
+ }
90
+ const missing = expectedHeaders.filter((header) => !normalized.includes(normalizeHeader(header)));
91
+ if (missing.length > 0) {
92
+ errors.push(`${label} table is missing column(s): ${missing.join(", ")}`);
93
+ }
94
+ return { rows, headers: normalized };
95
+ }
96
+ errors.push(`${label} section must include a markdown table with columns: ${expectedHeaders.join(" | ")}`);
97
+ return undefined;
98
+ }
99
+ export function cell(row, header) {
100
+ return row.cells[normalizeHeader(header)] ?? "";
101
+ }
102
+ export function isBlankish(value) {
103
+ if (Array.isArray(value)) {
104
+ return value.length === 0 || value.every(isBlankish);
105
+ }
106
+ if (value === undefined || value === null) {
107
+ return true;
108
+ }
109
+ const text = String(value).trim().toLowerCase();
110
+ return text === "" || text === "-" || text === "n/a" || text === "na" || text === "none" || text === "[]" || text === "not applicable";
111
+ }
112
+ export function weakProofHit(text) {
113
+ return WEAK_PROOF_PATTERNS.find((pattern) => pattern.test(text))?.source;
114
+ }
115
+ export async function assertReferencedPathsExist(projectRoot, label, text, errors) {
116
+ for (const reference of extractPathReferences(text)) {
117
+ const absolute = path.resolve(projectRoot, reference);
118
+ if (!isInside(projectRoot, absolute)) {
119
+ errors.push(`${label} references a path outside the project root: ${reference}`);
120
+ continue;
121
+ }
122
+ if (!(await pathExists(absolute))) {
123
+ errors.push(`${label} references missing path: ${reference}`);
124
+ }
125
+ }
126
+ }
127
+ export function primitiveText(value) {
128
+ if (value === undefined || value === null) {
129
+ return "";
130
+ }
131
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
132
+ return String(value);
133
+ }
134
+ if (Array.isArray(value)) {
135
+ return value.map(primitiveText).join(" ");
136
+ }
137
+ if (typeof value === "object") {
138
+ return Object.values(value).map(primitiveText).join(" ");
139
+ }
140
+ return "";
141
+ }
142
+ export function valuesAsArray(value) {
143
+ if (Array.isArray(value)) {
144
+ return value.flatMap(valuesAsArray);
145
+ }
146
+ if (isBlankish(value)) {
147
+ return [];
148
+ }
149
+ return String(value)
150
+ .split(/[,;\n]/)
151
+ .map((item) => item.trim())
152
+ .filter(Boolean);
153
+ }
154
+ export function hasRealPageEvidence(text) {
155
+ return /\b(real[- ]page|browser|route|screen|screenshot|url|localhost|https?:\/\/|page)\b/i.test(text);
156
+ }
157
+ export function isUiFacing(text) {
158
+ return /\b(ui|browser|page|screen|console|frontend|route|surface)\b/i.test(text);
159
+ }
160
+ export function isRuntimeFacing(text) {
161
+ return /\b(runtime|worker|api|schema|endpoint|service|queue|daemon|runner)\b/i.test(text);
162
+ }
163
+ function sectionBody(content, heading) {
164
+ const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
165
+ const match = new RegExp(`^##\\s+${escaped}\\s*$`, "im").exec(content);
166
+ if (!match) {
167
+ return undefined;
168
+ }
169
+ const rest = content.slice(match.index + match[0].length);
170
+ const next = /^##\s+/m.exec(rest);
171
+ return next ? rest.slice(0, next.index) : rest;
172
+ }
173
+ function splitTableLine(line) {
174
+ return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((cellValue) => cellValue.trim());
175
+ }
176
+ function isSeparatorLine(line) {
177
+ return splitTableLine(line).every((value) => /^:?-{3,}:?$/.test(value.trim()));
178
+ }
179
+ function normalizeHeader(value) {
180
+ return value.replace(/`/g, "").trim().toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ");
181
+ }
182
+ function extractPathReferences(text) {
183
+ const references = new Set();
184
+ for (const match of text.matchAll(PATH_PATTERN)) {
185
+ const raw = (match[1] ?? match[2] ?? "").trim();
186
+ if (!raw || raw.includes("*") || /^https?:\/\//i.test(raw)) {
187
+ continue;
188
+ }
189
+ references.add(raw.replace(/\\/g, "/").replace(/^\.\//, "").replace(/[),.;:]+$/, ""));
190
+ }
191
+ return [...references];
192
+ }
193
+ function isInside(root, target) {
194
+ const relative = path.relative(root, target);
195
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
196
+ }
@@ -2,4 +2,4 @@ export interface ValidatorReport {
2
2
  info: string[];
3
3
  errors: string[];
4
4
  }
5
- export declare function runValidator(projectRoot: string, gate: string): Promise<ValidatorReport>;
5
+ export declare function runValidator(projectRoot: string, gate: string, args?: string[]): Promise<ValidatorReport>;
@@ -3,11 +3,15 @@ import { readConfig } from "./config.js";
3
3
  import { harnessPath, harnessRoot } from "./harness-root.js";
4
4
  import { listFiles, pathExists, readText } from "./fs.js";
5
5
  import { runModularityCheck } from "./modularity.js";
6
+ import { validatePlanAcceptance } from "./plan-acceptance-validator.js";
7
+ import { validatePlanContract } from "./plan-contract-validator.js";
6
8
  import { unsupportedSchemaMessage } from "./schema-guard.js";
7
9
  const VALIDATORS = {
8
10
  "validate-context": validateContext,
9
11
  "validate-code-modularity": validateCodeModularity,
10
- "validate-harness": validateHarness
12
+ "validate-harness": validateHarness,
13
+ "validate-plan-contract": validatePlanContract,
14
+ "validate-plan-acceptance": validatePlanAcceptance
11
15
  };
12
16
  const GLOBAL_REQUIRED_SECTIONS = [
13
17
  ...sectionSpecs([
@@ -67,17 +71,17 @@ const FAKE_VERIFICATION_PATTERNS = [
67
71
  /\b验证(?:已)?通过\b/,
68
72
  /\b部署(?:已)?成功\b/
69
73
  ];
70
- export async function runValidator(projectRoot, gate) {
74
+ export async function runValidator(projectRoot, gate, args = []) {
71
75
  const validator = VALIDATORS[gate];
72
76
  if (!validator) {
73
77
  return {
74
78
  info: [],
75
79
  errors: [
76
- `unknown validator: ${gate}. Minimal Context Harness supports validate-context, validate-code-modularity and validate-harness only.`
80
+ `unknown validator: ${gate}. Minimal Context Harness supports validate-context, validate-code-modularity, validate-harness, validate-plan-contract and validate-plan-acceptance only.`
77
81
  ]
78
82
  };
79
83
  }
80
- return validator(projectRoot);
84
+ return validator(projectRoot, args);
81
85
  }
82
86
  async function validateHarness(projectRoot) {
83
87
  const contextReport = await validateContext(projectRoot);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-tiny-context-harness",
3
- "version": "0.2.69",
3
+ "version": "0.2.71",
4
4
  "description": "Minimal project memory and validation harness for AI coding agents.",
5
5
  "license": "MIT",
6
6
  "author": "Seven128",