agent-doc-system 1.0.0 → 1.2.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/CHANGELOG.md +269 -0
- package/README.md +97 -112
- package/adapters/ansible.mjs +306 -0
- package/adapters/cache.mjs +8 -2
- package/adapters/contract-consumers.mjs +1 -1
- package/adapters/database.mjs +18 -4
- package/adapters/events.mjs +1 -1
- package/adapters/github-actions.mjs +244 -85
- package/adapters/go.mjs +1 -1
- package/adapters/index.mjs +5 -2
- package/adapters/object-store.mjs +2 -2
- package/adapters/python.mjs +1142 -0
- package/adapters/resource-client.mjs +1 -1
- package/adapters/rust.mjs +987 -0
- package/adapters/typescript.mjs +1 -1
- package/bin/agentdoc.mjs +60 -44
- package/bin/commands.mjs +169 -0
- package/bin/usage.txt +5 -3
- package/core/audit.mjs +55 -4
- package/core/authority/engine.mjs +27 -4
- package/core/codes.mjs +11 -0
- package/core/compile.mjs +263 -13
- package/core/dependents.mjs +42 -0
- package/core/descriptors.mjs +5 -2
- package/core/discover.mjs +60 -9
- package/core/fsx.mjs +27 -4
- package/core/graph.mjs +1 -1
- package/core/impact.mjs +55 -44
- package/core/normative.mjs +171 -0
- package/core/query.mjs +18 -8
- package/core/scaffold.mjs +87 -22
- package/core/sourcescan.mjs +11 -2
- package/core/toml.mjs +463 -140
- package/core/yaml.mjs +5 -3
- package/docs/ADAPTERS.md +232 -7
- package/docs/AUTHORITY.md +10 -1
- package/docs/BENCHMARK.md +68 -0
- package/docs/CI.md +3 -1
- package/docs/EVALS.md +33 -2
- package/docs/MIGRATION.md +76 -6
- package/docs/QUERY.md +53 -4
- package/docs/SPEC.md +22 -0
- package/docs/assets/readme/agentdoc-hero.webp +0 -0
- package/docs/assets/readme/agentdoc-in-action.webp +0 -0
- package/docs/assets/readme/how-agentdoc-works.webp +0 -0
- package/docs/assets/readme/why-agentdoc.webp +0 -0
- package/docs/ci/github-actions.yml +1 -1
- package/evals/harness.mjs +32 -6
- package/evals/scenarios/fixture-b-03-change-shared-lib.json +8 -1
- package/evals/scenarios/fixture-c-03-shared-vocabulary.json +7 -1
- package/evals/scenarios/fixture-e-01-change-shared-lib.json +14 -0
- package/evals/scenarios/fixture-e-02-change-manifest.json +9 -0
- package/evals/scenarios/fixture-e-03-local-module.json +9 -0
- package/evals/scenarios/fixture-e-04-ambiguous-crate.json +14 -0
- package/evals/scenarios/fixture-f-01-change-shared-lib.json +9 -0
- package/evals/scenarios/fixture-f-02-change-manifest.json +9 -0
- package/evals/scenarios/fixture-f-03-shadowed-import.json +9 -0
- package/evals/scenarios/fixture-f-04-nested-workspace.json +9 -0
- package/evals/scenarios/fixture-f-05-alembic-database.json +9 -0
- package/evals/scenarios/fixture-f-06-unittest-job.json +9 -0
- package/package.json +2 -1
- package/schemas/common.schema.json +40 -0
- package/schemas/component.schema.json +5 -1
- package/schemas/config.schema.json +117 -1
- package/skills/agent-doc-system/SKILL.md +5 -1
- package/skills/agent-doc-system/references/cli.md +12 -4
- package/skills/agent-doc-system/references/create-migrate.md +28 -6
- package/templates/ci.yml +1 -1
package/core/compile.mjs
CHANGED
|
@@ -17,8 +17,13 @@ import { redactCommand } from "./secrets.mjs";
|
|
|
17
17
|
import { formatErrors } from "./jsonschema.mjs";
|
|
18
18
|
import { validateContractFile } from "./contracts.mjs";
|
|
19
19
|
import { sourceFiles, importSpecifiers } from "./sourcescan.mjs";
|
|
20
|
+
import { readPythonUnit, pythonImportNames } from "../adapters/python.mjs";
|
|
20
21
|
import { applyWarningAcceptances } from "./acceptances.mjs";
|
|
22
|
+
import {
|
|
23
|
+
loadNormativeDocuments, verifyCitation, normativeCoverage, isNormative, isRequirementKey, NORMATIVE_SEMANTICS_PREFIX,
|
|
24
|
+
} from "./normative.mjs";
|
|
21
25
|
import { loadAdapters } from "../adapters/index.mjs";
|
|
26
|
+
import { loadCargoUnit, analyzeCargoDeps, rustIdentsInSource } from "../adapters/rust.mjs";
|
|
22
27
|
import {
|
|
23
28
|
COMPILER_NAME, COMPILER_VERSION, GRAPH_SCHEMA_VERSION,
|
|
24
29
|
assembleGraph, serializeGraph, finalizeSerialization, subjectResolver,
|
|
@@ -115,16 +120,18 @@ export function compile(root, opts = {}) {
|
|
|
115
120
|
evidence: o.evidence.map((ev) => ({ path: ev.path, sha256: ev.sha256 })),
|
|
116
121
|
});
|
|
117
122
|
|
|
123
|
+
// ── normative requirements ───────────────────────────────────────────────
|
|
124
|
+
// Checked before discovery so a stale citation is reported alongside every
|
|
125
|
+
// other authored-input error rather than hiding behind a coverage failure.
|
|
126
|
+
const normative = loadNormative(repo, cfg, sources, overrides.overrides);
|
|
127
|
+
res.errors.push(...normative.errors);
|
|
128
|
+
res.normative = { sections: normative.sections, uncovered: normative.uncovered, requirements: normative.requirements };
|
|
129
|
+
|
|
118
130
|
res.errors.push(...checkContractCoverage({ repo, cfg, sources }));
|
|
119
131
|
|
|
120
132
|
for (const api of sources.entities.filter((e) => e.kind === "API")) {
|
|
121
133
|
collect(() => validateContractFile(repo, bundle, api), res.errors);
|
|
122
134
|
}
|
|
123
|
-
res.errors.push(...validateOidcProviders({ repo, sources, discovery: null }));
|
|
124
|
-
|
|
125
|
-
if (res.errors.length) {
|
|
126
|
-
return earlyOut(res, { entities: sources.entities.length });
|
|
127
|
-
}
|
|
128
135
|
|
|
129
136
|
let adapters;
|
|
130
137
|
try {
|
|
@@ -137,16 +144,22 @@ export function compile(root, opts = {}) {
|
|
|
137
144
|
throw e;
|
|
138
145
|
}
|
|
139
146
|
const discovery = discoverUnits(repo, cfg, adapters);
|
|
147
|
+
repo.nestedUnitRoots = discovery.nestedRoots;
|
|
140
148
|
// Eligibility is the compiler's own answer to "which directories are units" —
|
|
141
149
|
// exposed on the result so scaffold can write descriptors for exactly these
|
|
142
150
|
// roots rather than re-deriving them from filesystem markers.
|
|
143
151
|
res.discovery = discovery;
|
|
144
|
-
const match = matchComponents(discovery.eligible, sources.entities.filter((e) => e.kind === "Component"));
|
|
145
|
-
res.errors.push(...match.errors);
|
|
146
152
|
|
|
147
|
-
//
|
|
148
|
-
//
|
|
153
|
+
// A provider descriptor sits at its unit root. Check its source only after
|
|
154
|
+
// ownership is known, while still reporting this error alongside other
|
|
155
|
+
// descriptor/contract errors rather than hiding it behind an early return.
|
|
149
156
|
res.errors.push(...validateOidcProviders({ repo, sources, discovery }));
|
|
157
|
+
if (res.errors.length) {
|
|
158
|
+
return earlyOut(res, { entities: sources.entities.length });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const match = matchComponents(discovery.eligible, sources.entities.filter((e) => e.kind === "Component"), cfg);
|
|
162
|
+
res.errors.push(...match.errors);
|
|
150
163
|
|
|
151
164
|
if (res.errors.length) {
|
|
152
165
|
return earlyOut(res, { entities: sources.entities.length, units: discovery.eligible.length });
|
|
@@ -165,6 +178,9 @@ export function compile(root, opts = {}) {
|
|
|
165
178
|
repo, cfg, refs, bundle, prov, diagnostics, facts,
|
|
166
179
|
sources: { ...sources, journeys: journeys.journeys }, discovery, journeys: journeys.journeys, journeysPath: journeys.path,
|
|
167
180
|
observations, overrides: overrides.overrides,
|
|
181
|
+
// Declared normative requirements, so an adapter named in `satisfiedBy`
|
|
182
|
+
// can derive the implementation side on the same key.
|
|
183
|
+
requirements: normative.requirements,
|
|
168
184
|
edges, events, externals, capabilities, verification,
|
|
169
185
|
errors: [],
|
|
170
186
|
unitByComponent: new Map(),
|
|
@@ -272,6 +288,17 @@ export function compile(root, opts = {}) {
|
|
|
272
288
|
// ── manifest pre-pass: identify each unit's package / module namespace ────
|
|
273
289
|
// Done before extraction so cross-unit resolution never depends on the order
|
|
274
290
|
// in which units happen to be processed.
|
|
291
|
+
// Cargo.toml is read only when the rust adapter is on. An existence probe is
|
|
292
|
+
// part of the input hash, so probing it for every unit would change every
|
|
293
|
+
// graph that does not enable rust.
|
|
294
|
+
const rustOn = adapters.some((a) => a.name === "rust");
|
|
295
|
+
ctx.unitByCrateName = new Map();
|
|
296
|
+
ctx.ambiguousCrateNames = new Map();
|
|
297
|
+
ctx.rustAmbiguity = new Map();
|
|
298
|
+
// A Python manifest is read only when the python adapter is on. An existence
|
|
299
|
+
// probe is part of the input hash, so probing one for every unit would change
|
|
300
|
+
// every graph that does not enable python.
|
|
301
|
+
const pythonOn = adapters.some((a) => a.name === "python");
|
|
275
302
|
for (const unit of discovery.eligible) {
|
|
276
303
|
const pkgPath = unit.root + "/package.json";
|
|
277
304
|
if (repo.exists(pkgPath)) {
|
|
@@ -288,15 +315,68 @@ export function compile(root, opts = {}) {
|
|
|
288
315
|
const m = /^module\s+(\S+)/m.exec(repo.readText(gomodPath));
|
|
289
316
|
unit.goModule = m ? m[1] : null;
|
|
290
317
|
}
|
|
318
|
+
if (rustOn) loadCargoUnit(repo, unit);
|
|
319
|
+
if (pythonOn) {
|
|
320
|
+
const py = readPythonUnit(repo, unit.root);
|
|
321
|
+
if (py.manifest) {
|
|
322
|
+
unit.pythonMarker = py.manifest;
|
|
323
|
+
unit.pythonManifest = py.manifest;
|
|
324
|
+
unit.pythonDist = py.dist;
|
|
325
|
+
unit.pythonImports = py.ambiguousImports ? null : py.importNames;
|
|
326
|
+
unit.pythonProdDeps = [...new Set([
|
|
327
|
+
...py.prodDeps,
|
|
328
|
+
...py.sourceDeps.filter((n) => !py.devDeps.includes(n) && !py.prodDeps.includes(n)),
|
|
329
|
+
])].sort();
|
|
330
|
+
unit.pythonDevDeps = py.devDeps;
|
|
331
|
+
unit.pythonPathDeps = py.pathDeps;
|
|
332
|
+
if (py.error) res.errors.push(py.error);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
291
335
|
}
|
|
292
336
|
const unitByPkgName = new Map();
|
|
293
337
|
const unitByGoModule = new Map();
|
|
338
|
+
const unitByPythonDist = new Map();
|
|
339
|
+
const unitByPythonImport = new Map();
|
|
340
|
+
const distOwners = new Map();
|
|
341
|
+
const importOwners = new Map();
|
|
294
342
|
for (const unit of discovery.eligible) {
|
|
295
343
|
if (unit.pkgName && !unitByPkgName.has(unit.pkgName)) unitByPkgName.set(unit.pkgName, unit);
|
|
296
344
|
if (unit.goModule && !unitByGoModule.has(unit.goModule)) unitByGoModule.set(unit.goModule, unit);
|
|
345
|
+
if (unit.pythonDist) {
|
|
346
|
+
if (!distOwners.has(unit.pythonDist)) distOwners.set(unit.pythonDist, []);
|
|
347
|
+
distOwners.get(unit.pythonDist).push(unit);
|
|
348
|
+
}
|
|
349
|
+
if (Array.isArray(unit.pythonImports)) {
|
|
350
|
+
for (const name of unit.pythonImports) {
|
|
351
|
+
if (!importOwners.has(name)) importOwners.set(name, []);
|
|
352
|
+
importOwners.get(name).push(unit);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
for (const [name, owners] of distOwners) {
|
|
357
|
+
if (owners.length === 1) unitByPythonDist.set(name, owners[0]);
|
|
358
|
+
else for (const u of owners) u.pythonDistConflict = true;
|
|
359
|
+
}
|
|
360
|
+
for (const [name, owners] of importOwners) {
|
|
361
|
+
if (owners.length === 1) unitByPythonImport.set(name, owners[0]);
|
|
362
|
+
else for (const u of owners) u.pythonImportConflict = true;
|
|
297
363
|
}
|
|
298
364
|
ctx.unitByPkgName = unitByPkgName;
|
|
299
365
|
ctx.unitByGoModule = unitByGoModule;
|
|
366
|
+
if (rustOn) {
|
|
367
|
+
const owners = new Map();
|
|
368
|
+
for (const unit of discovery.eligible) {
|
|
369
|
+
if (!unit.crateName) continue;
|
|
370
|
+
if (!owners.has(unit.crateName)) owners.set(unit.crateName, []);
|
|
371
|
+
owners.get(unit.crateName).push(unit);
|
|
372
|
+
}
|
|
373
|
+
for (const [name, list] of [...owners.entries()].sort((a, b) => (a[0] < b[0] ? -1 : 1))) {
|
|
374
|
+
if (list.length === 1) ctx.unitByCrateName.set(name, list[0]);
|
|
375
|
+
else ctx.ambiguousCrateNames.set(name, list.slice().sort((a, b) => (a.root < b.root ? -1 : 1)));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
ctx.unitByPythonDist = unitByPythonDist;
|
|
379
|
+
ctx.unitByPythonImport = unitByPythonImport;
|
|
300
380
|
|
|
301
381
|
// Manifest-declared unit dependencies, computed before any adapter runs so
|
|
302
382
|
// that cross-unit reasoning never depends on adapter execution order.
|
|
@@ -304,6 +384,7 @@ export function compile(root, opts = {}) {
|
|
|
304
384
|
// matches a dependency name rather than a blob of source.
|
|
305
385
|
const productionDependencies = new Map();
|
|
306
386
|
const dependencyMap = new Map();
|
|
387
|
+
const unitsByRoot = new Map(discovery.eligible.map((u) => [u.root, u]));
|
|
307
388
|
for (const unit of discovery.eligible) {
|
|
308
389
|
if (!unit.component) continue;
|
|
309
390
|
const deps = new Set();
|
|
@@ -325,6 +406,32 @@ export function compile(root, opts = {}) {
|
|
|
325
406
|
if (t && t.component && t.component.ref !== unit.component.ref) deps.add(t.component.ref);
|
|
326
407
|
}
|
|
327
408
|
}
|
|
409
|
+
if (rustOn && unit.cargoPackage) {
|
|
410
|
+
const cargo = analyzeCargoDeps(unit, unitsByRoot);
|
|
411
|
+
productionDependencies.set(unit.component.ref, [
|
|
412
|
+
...(productionDependencies.get(unit.component.ref) || []),
|
|
413
|
+
...cargo.prodNames,
|
|
414
|
+
]);
|
|
415
|
+
for (const hit of cargo.resolved) {
|
|
416
|
+
if (hit.ambiguous || !hit.target || !hit.target.component) continue;
|
|
417
|
+
if (hit.target.component.ref !== unit.component.ref) deps.add(hit.target.component.ref);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (unit.pythonMarker) {
|
|
421
|
+
const pyNames = [...new Set([...(unit.pythonProdDeps || []), ...(unit.pythonDevDeps || [])])];
|
|
422
|
+
productionDependencies.set(unit.component.ref, [
|
|
423
|
+
...(productionDependencies.get(unit.component.ref) || []),
|
|
424
|
+
...(unit.pythonProdDeps || []),
|
|
425
|
+
]);
|
|
426
|
+
for (const name of pyNames) {
|
|
427
|
+
const t = unitByPythonDist.get(name);
|
|
428
|
+
if (t && t.component && t.component.ref !== unit.component.ref) deps.add(t.component.ref);
|
|
429
|
+
}
|
|
430
|
+
for (const p of unit.pythonPathDeps || []) {
|
|
431
|
+
const t = discovery.eligible.find((u) => u.root === p.dir);
|
|
432
|
+
if (t && t.component && t.component.ref !== unit.component.ref) deps.add(t.component.ref);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
328
435
|
dependencyMap.set(unit.component.ref, deps);
|
|
329
436
|
}
|
|
330
437
|
ctx.dependencyMap = dependencyMap;
|
|
@@ -436,6 +543,21 @@ export function compile(root, opts = {}) {
|
|
|
436
543
|
}
|
|
437
544
|
}
|
|
438
545
|
|
|
546
|
+
// A requirement is an AUTHORED claim that the implementation must make true.
|
|
547
|
+
// It is a fact like any other so the authority engine compares it with the
|
|
548
|
+
// implementation evidence on the same key — and it carries its citation, so
|
|
549
|
+
// every report can say which section of which document it came from.
|
|
550
|
+
for (const r of normative.requirements) {
|
|
551
|
+
facts.add({
|
|
552
|
+
subject: r.subject, key: r.key, value: r.value, evidenceClass: "AUTHORED", confidence: "declared",
|
|
553
|
+
provRecs: [
|
|
554
|
+
prov.add("declared", r.file, "requirement-extractor", ["ass:pending"]),
|
|
555
|
+
prov.add("declared", r.source.path, "requirement-extractor", ["ass:pending"]),
|
|
556
|
+
],
|
|
557
|
+
semantics: NORMATIVE_SEMANTICS_PREFIX + r.source.path + "#" + r.source.section,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
439
561
|
// ── reviewed overrides → relations and facts ─────────────────────────────
|
|
440
562
|
const overrideEdges = [];
|
|
441
563
|
for (const o of overrides.overrides) {
|
|
@@ -503,6 +625,13 @@ export function compile(root, opts = {}) {
|
|
|
503
625
|
evidenceClass: "REVIEWED_OVERRIDE", confidence: "reviewed", provRecs: provs, semantics: o.reason,
|
|
504
626
|
review: reviewed(o),
|
|
505
627
|
});
|
|
628
|
+
} else if (o.fact === "requirement-satisfaction") {
|
|
629
|
+
facts.add({
|
|
630
|
+
subject: o.subject, key: o.key, value: o.value,
|
|
631
|
+
evidenceClass: "REVIEWED_OVERRIDE", confidence: "reviewed", provRecs: provs,
|
|
632
|
+
semantics: o.reason,
|
|
633
|
+
review: reviewed(o),
|
|
634
|
+
});
|
|
506
635
|
} else if (o.fact === "component-placement") {
|
|
507
636
|
const key = o.system ? "placement.system" : "placement.domain";
|
|
508
637
|
facts.add({
|
|
@@ -557,6 +686,7 @@ export function compile(root, opts = {}) {
|
|
|
557
686
|
|
|
558
687
|
const { conflicts: rawConflicts } = resolveAssertions(facts.facts, authority);
|
|
559
688
|
pushConflictDiagnostics(rawConflicts, diagnostics, facts);
|
|
689
|
+
pushNormativeDiagnostics(normative, facts, adapters, diagnostics);
|
|
560
690
|
|
|
561
691
|
// ── relations ────────────────────────────────────────────────────────────
|
|
562
692
|
const rel = buildRelations(ctx);
|
|
@@ -753,7 +883,13 @@ function pushConflictDiagnostics(rawConflicts, diagnostics, facts) {
|
|
|
753
883
|
.filter(Boolean)
|
|
754
884
|
.map((f) => f.evidenceClass + "=" + JSON.stringify(f.value))
|
|
755
885
|
.sort();
|
|
756
|
-
|
|
886
|
+
let detail = c.subject + " " + c.key + ": " + values.join(" vs ");
|
|
887
|
+
// For a requirement, say where each side comes from: the cited section and
|
|
888
|
+
// what the extractor actually found. Two bare values send a reader hunting.
|
|
889
|
+
if (isRequirementKey(c.key)) {
|
|
890
|
+
const why = c.assertionIds.map((id) => byId.get(id)).filter((f) => f && f.semantics).map((f) => f.semantics).sort();
|
|
891
|
+
if (why.length) detail += " (" + why.join("; ") + ")";
|
|
892
|
+
}
|
|
757
893
|
if (c.kind === "ambiguity" && c.status === "unresolved") {
|
|
758
894
|
diagnostics.push({
|
|
759
895
|
severity: "warning",
|
|
@@ -775,7 +911,7 @@ function pushConflictDiagnostics(rawConflicts, diagnostics, facts) {
|
|
|
775
911
|
message:
|
|
776
912
|
"unresolved " + c.kind + " — " + detail + ". " +
|
|
777
913
|
(c.election.rationale || "no authority rule governs this fact") +
|
|
778
|
-
". Add an authority rule and, if deterministic discovery cannot settle it, a reviewed override.",
|
|
914
|
+
(isRequirementKey(c.key) ? "." : ". Add an authority rule and, if deterministic discovery cannot settle it, a reviewed override."),
|
|
779
915
|
});
|
|
780
916
|
} else {
|
|
781
917
|
diagnostics.push({
|
|
@@ -794,6 +930,101 @@ function pushConflictDiagnostics(rawConflicts, diagnostics, facts) {
|
|
|
794
930
|
}
|
|
795
931
|
}
|
|
796
932
|
|
|
933
|
+
// Normative documents, the requirements components declare against them, and
|
|
934
|
+
// which sections nobody has accounted for. Every citation is re-verified
|
|
935
|
+
// against the section's current digest: a requirement or an attestation
|
|
936
|
+
// written against an older wording of the specification is a review that has
|
|
937
|
+
// not happened yet.
|
|
938
|
+
function loadNormative(repo, cfg, sources, overrides) {
|
|
939
|
+
const errors = [];
|
|
940
|
+
const out = { errors, sections: [], uncovered: [], requirements: [] };
|
|
941
|
+
let docs;
|
|
942
|
+
try {
|
|
943
|
+
docs = loadNormativeDocuments(repo, cfg);
|
|
944
|
+
} catch (e) {
|
|
945
|
+
if (!(e instanceof AgentDocError)) throw e;
|
|
946
|
+
errors.push(e);
|
|
947
|
+
return out;
|
|
948
|
+
}
|
|
949
|
+
for (const e of sources.entities) {
|
|
950
|
+
if (e.kind !== "Component") continue;
|
|
951
|
+
const seen = new Set();
|
|
952
|
+
for (const r of e.doc.spec.requirements || []) {
|
|
953
|
+
collect(() => {
|
|
954
|
+
if (seen.has(r.id) || seen.has(r.key)) {
|
|
955
|
+
throw new AgentDocError(CODES.CONFIG, "duplicate requirement id or key on " + e.ref + ": " + r.id + " / " + r.key, { path: e.file, ref: e.ref });
|
|
956
|
+
}
|
|
957
|
+
seen.add(r.id);
|
|
958
|
+
seen.add(r.key);
|
|
959
|
+
verifyCitation(docs, r.source, e.file, e.ref);
|
|
960
|
+
out.requirements.push({ ...r, subject: e.ref, file: e.file, satisfiedBy: r.satisfiedBy || [] });
|
|
961
|
+
}, errors);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
// A satisfaction override must answer a requirement someone declared. One
|
|
965
|
+
// that answers nothing would be an implementation claim with no obligation
|
|
966
|
+
// behind it, which no report could ever surface.
|
|
967
|
+
const declared = new Set(out.requirements.map((r) => r.subject + "\u0000" + r.key));
|
|
968
|
+
for (const o of overrides) {
|
|
969
|
+
if (o.fact !== "requirement-satisfaction") continue;
|
|
970
|
+
if (!declared.has(o.subject + "\u0000" + o.key)) {
|
|
971
|
+
errors.push(new AgentDocError(
|
|
972
|
+
CODES.CONFIG,
|
|
973
|
+
"requirement-satisfaction override answers no declared requirement: " + o.subject + " " + o.key,
|
|
974
|
+
{ path: CONFIG_PATH, ref: o.subject }
|
|
975
|
+
));
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
const cov = normativeCoverage(docs, out.requirements, cfg.normativeCoverage || [], errors);
|
|
979
|
+
out.sections = cov.sections;
|
|
980
|
+
out.uncovered = cov.uncovered;
|
|
981
|
+
return out;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function pushNormativeDiagnostics(normative, facts, adapters, diagnostics) {
|
|
985
|
+
for (const s of normative.uncovered) {
|
|
986
|
+
const id = s.path + "#" + s.section;
|
|
987
|
+
diagnostics.push({
|
|
988
|
+
severity: "warning",
|
|
989
|
+
code: CODES.NORMATIVE_UNCOVERED,
|
|
990
|
+
subject: id,
|
|
991
|
+
paths: [s.path],
|
|
992
|
+
refs: [],
|
|
993
|
+
message:
|
|
994
|
+
"normative section " + id + " (sha256 " + s.sha256 + ") is routed but not reviewed: " +
|
|
995
|
+
"declare the requirement it places on a component (spec.requirements), or attest in normativeCoverage " +
|
|
996
|
+
"that it carries no implementation obligation.",
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
const enabled = new Set(adapters.map((a) => a.name));
|
|
1000
|
+
for (const r of normative.requirements) {
|
|
1001
|
+
const group = facts.facts.filter((f) => f.subject === r.subject && f.key === r.key && f.status !== "superseded");
|
|
1002
|
+
if (group.some((f) => !isNormative(f) && f.confidence !== "candidate")) continue;
|
|
1003
|
+
const candidate = group.find((f) => !isNormative(f));
|
|
1004
|
+
const missing = r.satisfiedBy.filter((a) => !enabled.has(a));
|
|
1005
|
+
const why = candidate
|
|
1006
|
+
? "only a heuristic candidate exists, which is never elected — " + (candidate.semantics || JSON.stringify(candidate.value))
|
|
1007
|
+
: !r.satisfiedBy.length
|
|
1008
|
+
? "no adapter is named as able to derive it"
|
|
1009
|
+
: missing.length
|
|
1010
|
+
? "the adapter able to derive it is not enabled or not available: " + missing.join(", ")
|
|
1011
|
+
: !r.expects
|
|
1012
|
+
? "the requirement has no `expects`, so " + r.satisfiedBy.join(", ") + " has nothing to look for"
|
|
1013
|
+
: "the enabled adapter (" + r.satisfiedBy.join(", ") + ") derived no evidence for it";
|
|
1014
|
+
diagnostics.push({
|
|
1015
|
+
severity: "warning",
|
|
1016
|
+
code: CODES.REQUIREMENT_UNVERIFIED,
|
|
1017
|
+
subject: r.subject + "#" + r.key,
|
|
1018
|
+
paths: [r.source.path],
|
|
1019
|
+
refs: [r.subject],
|
|
1020
|
+
message:
|
|
1021
|
+
"requirement " + r.key + " = " + r.value + " on " + r.subject + " (from " + r.source.path + "#" + r.source.section +
|
|
1022
|
+
") has no implementation evidence; " + why + ". Add a requirement-satisfaction reviewed override citing the " +
|
|
1023
|
+
"implementation, or accept the gap with review.",
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
797
1028
|
// A Go import is either host.tld/owner/repo/path or host.tld/owner/repo. The
|
|
798
1029
|
// second form is what a Go *workspace* member uses, and it has only two dots —
|
|
799
1030
|
// a pattern that requires three silently loses every workspace-only dependency.
|
|
@@ -805,9 +1036,13 @@ const GO_IMPORT_RE = /["`]([a-z0-9]+\.[a-z0-9.-]+\/[a-z0-9./_-]+|[a-z0-9]+\.[a-z
|
|
|
805
1036
|
function buildImportIndex(repo, discovery) {
|
|
806
1037
|
const byUnit = new Map();
|
|
807
1038
|
const goPathsByUnit = new Map();
|
|
1039
|
+
const rustIdentsByUnit = new Map();
|
|
1040
|
+
const pythonImportsByUnit = new Map();
|
|
808
1041
|
for (const unit of discovery.eligible) {
|
|
809
1042
|
const map = new Map();
|
|
810
1043
|
const goPaths = new Set();
|
|
1044
|
+
const rustIdents = new Map();
|
|
1045
|
+
const pyImports = new Map();
|
|
811
1046
|
for (const f of sourceFiles(repo, unit.root, { test: false })) {
|
|
812
1047
|
const specifiers = importSpecifiers(repo.readText(f));
|
|
813
1048
|
for (const s of specifiers) {
|
|
@@ -821,11 +1056,26 @@ function buildImportIndex(repo, discovery) {
|
|
|
821
1056
|
// importing it, with no require line at all.
|
|
822
1057
|
for (const m of text.matchAll(GO_IMPORT_RE) ) goPaths.add(m[1]);
|
|
823
1058
|
}
|
|
1059
|
+
if (unit.cargoPackage && f.endsWith(".rs")) {
|
|
1060
|
+
const text = repo.readText(f);
|
|
1061
|
+
for (const ident of rustIdentsInSource(text)) {
|
|
1062
|
+
if (!rustIdents.has(ident)) rustIdents.set(ident, new Set());
|
|
1063
|
+
rustIdents.get(ident).add(f);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
if (unit.pythonMarker && f.endsWith(".py")) {
|
|
1067
|
+
for (const name of pythonImportNames(repo.readText(f))) {
|
|
1068
|
+
if (!pyImports.has(name)) pyImports.set(name, new Set());
|
|
1069
|
+
pyImports.get(name).add(f);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
824
1072
|
}
|
|
825
1073
|
byUnit.set(unit, map);
|
|
826
1074
|
goPathsByUnit.set(unit, goPaths);
|
|
1075
|
+
rustIdentsByUnit.set(unit, rustIdents);
|
|
1076
|
+
pythonImportsByUnit.set(unit, pyImports);
|
|
827
1077
|
}
|
|
828
|
-
return { byUnit, goPathsByUnit };
|
|
1078
|
+
return { byUnit, goPathsByUnit, rustIdentsByUnit, pythonImportsByUnit };
|
|
829
1079
|
}
|
|
830
1080
|
|
|
831
1081
|
function mergeDerived(target, extra) {
|
|
@@ -858,7 +1108,7 @@ function validateOidcProviders({ repo, sources, discovery }) {
|
|
|
858
1108
|
const roots = unit ? [unit.root] : [dirOf(provider.file)];
|
|
859
1109
|
let found = false;
|
|
860
1110
|
for (const root of roots) {
|
|
861
|
-
for (const f of repo.
|
|
1111
|
+
for (const f of repo.walkUnit(root)) {
|
|
862
1112
|
if (!/\.(ts|tsx|js|mjs|jsx|py|go|rb|java|kt|rs)$/.test(f)) continue;
|
|
863
1113
|
if (/(^|\/)(test|tests|__tests__)\//.test(f) || /\.(test|spec)\./.test(f)) continue;
|
|
864
1114
|
if (repo.readText(f).includes(dp)) { found = true; break; }
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Change routing follows dependency edges in reverse. Normalize inverse edges
|
|
2
|
+
// here so query and impact agree even when the graph includes both spellings.
|
|
3
|
+
export function dependentScope(graph, seeds) {
|
|
4
|
+
const reverse = new Map();
|
|
5
|
+
for (const r of graph.relations) {
|
|
6
|
+
let dependency, dependent;
|
|
7
|
+
if (r.type === "buildDependsOn" || r.type === "testDependsOn") {
|
|
8
|
+
dependency = r.targetRef;
|
|
9
|
+
dependent = r.sourceRef;
|
|
10
|
+
} else if (r.type === "buildDependencyOf" || r.type === "testDependencyOf") {
|
|
11
|
+
dependency = r.sourceRef;
|
|
12
|
+
dependent = r.targetRef;
|
|
13
|
+
} else continue;
|
|
14
|
+
if (!reverse.has(dependency)) reverse.set(dependency, []);
|
|
15
|
+
reverse.get(dependency).push({ ref: dependent, unitOnly: r.type.startsWith("test") });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// false means all tiers; true means only unit checks. An all-build route
|
|
19
|
+
// upgrades a prior test-only route, including its downstream dependents.
|
|
20
|
+
const scope = new Map(seeds.map((ref) => [ref, false]));
|
|
21
|
+
const queue = [...scope.keys()];
|
|
22
|
+
for (let i = 0; i < queue.length; i++) {
|
|
23
|
+
const current = queue[i];
|
|
24
|
+
for (const edge of reverse.get(current) || []) {
|
|
25
|
+
const unitOnly = scope.get(current) || edge.unitOnly;
|
|
26
|
+
if (!scope.has(edge.ref) || (scope.get(edge.ref) && !unitOnly)) {
|
|
27
|
+
scope.set(edge.ref, unitOnly);
|
|
28
|
+
queue.push(edge.ref);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return scope;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function verificationForChange(graph, seeds, priorityRefs = seeds) {
|
|
36
|
+
const own = new Set(priorityRefs);
|
|
37
|
+
const scope = dependentScope(graph, seeds);
|
|
38
|
+
const rank = (v) => v.componentRefs.some((ref) => own.has(ref)) ? 0 : 1;
|
|
39
|
+
return graph.verification
|
|
40
|
+
.filter((v) => v.componentRefs.some((ref) => scope.has(ref) && (!scope.get(ref) || v.tier === "unit")))
|
|
41
|
+
.sort((a, b) => rank(a) - rank(b) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
42
|
+
}
|
package/core/descriptors.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import fs from "node:fs";
|
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import { createHash } from "node:crypto";
|
|
17
17
|
import { AgentDocError, CODES, collect } from "./codes.mjs";
|
|
18
|
-
import { assertRepoPath, expandGlob, sha256Hex } from "./fsx.mjs";
|
|
18
|
+
import { assertGlob, assertRepoPath, expandGlob, sha256Hex } from "./fsx.mjs";
|
|
19
19
|
import { parseYamlDocuments } from "./yaml.mjs";
|
|
20
20
|
import { scanForSecrets } from "./secrets.mjs";
|
|
21
21
|
import { formatErrors } from "./jsonschema.mjs";
|
|
@@ -160,10 +160,13 @@ export function loadConfig(repo, bundle) {
|
|
|
160
160
|
cfg.output.graph, cfg.output.observations,
|
|
161
161
|
...(cfg.discovery.supplementalRoots || []),
|
|
162
162
|
...(cfg.discovery.contractRoots || []),
|
|
163
|
-
...(cfg.docs ? Object.
|
|
163
|
+
...(cfg.docs ? Object.entries(cfg.docs).filter(([k]) => k !== "normative").map(([, v]) => v) : []),
|
|
164
164
|
].filter(Boolean)) {
|
|
165
165
|
assertRepoPath(p, CONFIG_PATH);
|
|
166
166
|
}
|
|
167
|
+
// `docs.normative` holds globs, not paths: the only role that names a set of
|
|
168
|
+
// documents rather than one.
|
|
169
|
+
for (const g of (cfg.docs && cfg.docs.normative) || []) assertGlob(g, CONFIG_PATH);
|
|
167
170
|
return cfg;
|
|
168
171
|
}
|
|
169
172
|
|
package/core/discover.mjs
CHANGED
|
@@ -8,12 +8,22 @@ import path from "node:path";
|
|
|
8
8
|
import { AgentDocError, CODES } from "./codes.mjs";
|
|
9
9
|
import { expandGlob, globToRegExp, assertGlob } from "../core/fsx.mjs";
|
|
10
10
|
|
|
11
|
-
//
|
|
12
|
-
//
|
|
11
|
+
// Roots excluded from inference and scaffolding. Authored descriptors strictly
|
|
12
|
+
// below them can still claim units; descriptors at these exact roots cannot.
|
|
13
13
|
export const DEFAULT_NOT_UNIT_ROOTS = ["agentdoc", "docs", "scripts", "node_modules", ".github"];
|
|
14
14
|
|
|
15
|
+
function notUnitRoots(cfg) {
|
|
16
|
+
return cfg?.discovery.notUnitRoots || DEFAULT_NOT_UNIT_ROOTS;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Inference excludes the whole subtree, not similarly named siblings. CREATE
|
|
20
|
+
// mode shares this policy even for units with an existing authored descriptor.
|
|
21
|
+
export function isNotUnitRoot(root, cfg) {
|
|
22
|
+
return notUnitRoots(cfg)
|
|
23
|
+
.some((excluded) => root === excluded || root.startsWith(excluded + "/"));
|
|
24
|
+
}
|
|
25
|
+
|
|
15
26
|
export function discoverUnits(repo, cfg, adapters) {
|
|
16
|
-
const notUnit = new Set(cfg.discovery.notUnitRoots || DEFAULT_NOT_UNIT_ROOTS);
|
|
17
27
|
const extraDeployable = new Set();
|
|
18
28
|
const extraImportable = new Set();
|
|
19
29
|
const units = new Map();
|
|
@@ -35,13 +45,17 @@ export function discoverUnits(repo, cfg, adapters) {
|
|
|
35
45
|
assertGlob(g, "agentdoc config");
|
|
36
46
|
for (const f of expandGlob(repo, g)) {
|
|
37
47
|
const dir = path.posix.dirname(f);
|
|
38
|
-
if (
|
|
48
|
+
if (repo.isDir(dir)) {
|
|
39
49
|
candidates.add(dir);
|
|
40
50
|
descriptorRoots.add(dir);
|
|
41
51
|
}
|
|
42
52
|
}
|
|
43
53
|
}
|
|
44
|
-
for (const r of cfg.discovery.supplementalRoots || [])
|
|
54
|
+
for (const r of cfg.discovery.supplementalRoots || []) {
|
|
55
|
+
candidates.add(r);
|
|
56
|
+
// The descriptor loader also reads these fixed paths outside the globs.
|
|
57
|
+
if (repo.exists(r + "/" + cfg.discovery.componentDescriptorName)) descriptorRoots.add(r);
|
|
58
|
+
}
|
|
45
59
|
|
|
46
60
|
// Adapters may contribute roots the descriptor globs cannot know about: a
|
|
47
61
|
// Go workspace member, a workspace file member with no descriptor yet. The
|
|
@@ -59,7 +73,10 @@ export function discoverUnits(repo, cfg, adapters) {
|
|
|
59
73
|
}
|
|
60
74
|
|
|
61
75
|
for (const root of [...candidates].sort()) {
|
|
62
|
-
|
|
76
|
+
// Preserve explicit human claims below an exclusion, while keeping inferred
|
|
77
|
+
// roots out of the subtree and refusing descriptors at the exact root.
|
|
78
|
+
if (notUnitRoots(cfg).includes(root)) continue;
|
|
79
|
+
if (isNotUnitRoot(root, cfg) && !descriptorRoots.has(root)) continue;
|
|
63
80
|
if (!repo.isDir(root)) continue;
|
|
64
81
|
const u = get(root);
|
|
65
82
|
u.explicit = (cfg.discovery.supplementalRoots || []).includes(root) || descriptorRoots.has(root);
|
|
@@ -72,13 +89,38 @@ export function discoverUnits(repo, cfg, adapters) {
|
|
|
72
89
|
const shape = markerShape(marker);
|
|
73
90
|
if (shape === "deployable" || shape === "both") u.deployable = true;
|
|
74
91
|
if (shape === "importable" || shape === "both") u.importable = true;
|
|
92
|
+
// A manifest with no artifact shape is still a unit. deployable and
|
|
93
|
+
// importable stay false, so scaffold does not invent a service or a
|
|
94
|
+
// library. A descriptor is not required before the unit is recognised.
|
|
95
|
+
if (shape === "present") u.present = true;
|
|
75
96
|
}
|
|
76
97
|
}
|
|
77
98
|
}
|
|
78
99
|
|
|
79
100
|
const list = [...units.values()].sort((a, b) => (a.root < b.root ? -1 : 1));
|
|
80
|
-
const eligible = list.filter((u) => u.deployable || u.importable || u.explicit);
|
|
81
|
-
|
|
101
|
+
const eligible = list.filter((u) => u.deployable || u.importable || u.explicit || u.present);
|
|
102
|
+
// Only eligible units create ownership boundaries. Record immediate children
|
|
103
|
+
// once; pruning one of them also prunes every deeper descendant. Normalise
|
|
104
|
+
// the repository root so descriptor roots (".") and workspace roots agree.
|
|
105
|
+
const nestedRoots = new Map(eligible.map((u) => [unitRoot(u.root), new Set()]));
|
|
106
|
+
for (const root of nestedRoots.keys()) {
|
|
107
|
+
if (!root) continue;
|
|
108
|
+
let parent = unitRoot(path.posix.dirname(root));
|
|
109
|
+
while (true) {
|
|
110
|
+
if (nestedRoots.has(parent)) {
|
|
111
|
+
nestedRoots.get(parent).add(root);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
if (!parent) break;
|
|
115
|
+
parent = unitRoot(path.posix.dirname(parent));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { units: list, eligible, nestedRoots };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function unitRoot(root) {
|
|
122
|
+
const normalized = path.posix.normalize(root || ".");
|
|
123
|
+
return normalized === "." ? "" : normalized;
|
|
82
124
|
}
|
|
83
125
|
|
|
84
126
|
function markerShape(marker) {
|
|
@@ -93,7 +135,7 @@ export function descriptorDir(file) {
|
|
|
93
135
|
}
|
|
94
136
|
|
|
95
137
|
// Exactly one descriptor per eligible unit, and no descriptor outside one.
|
|
96
|
-
export function matchComponents(eligible, componentEntities) {
|
|
138
|
+
export function matchComponents(eligible, componentEntities, cfg) {
|
|
97
139
|
const errors = [];
|
|
98
140
|
const byDir = new Map();
|
|
99
141
|
for (const e of componentEntities) {
|
|
@@ -124,6 +166,15 @@ export function matchComponents(eligible, componentEntities) {
|
|
|
124
166
|
for (const e of componentEntities) {
|
|
125
167
|
const dir = descriptorDir(e.file);
|
|
126
168
|
if (unitRoots.has(dir)) continue;
|
|
169
|
+
if (notUnitRoots(cfg).includes(dir)) {
|
|
170
|
+
errors.push(new AgentDocError(
|
|
171
|
+
CODES.COMPONENT_UNCORROBORATED,
|
|
172
|
+
"component descriptor at '" + dir + "' is in discovery.notUnitRoots (or a default not-unit root); " +
|
|
173
|
+
"remove it from notUnitRoots or move the descriptor",
|
|
174
|
+
{ path: e.file, ref: e.ref }
|
|
175
|
+
));
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
127
178
|
// A descriptor is a human claim that a component exists. When no adapter
|
|
128
179
|
// can corroborate a deployable or importable artifact in that source root,
|
|
129
180
|
// the claim is unsupported. This is reported rather than ignored: it is
|
package/core/fsx.mjs
CHANGED
|
@@ -59,6 +59,7 @@ export class Repo {
|
|
|
59
59
|
this.probes = new Map();
|
|
60
60
|
this.walks = new Map();
|
|
61
61
|
this.listings = new Map();
|
|
62
|
+
this.nestedUnitRoots = new Map();
|
|
62
63
|
this.generatedDirs = new Set(opts.generatedDirs || DEFAULT_GENERATED_DIRS);
|
|
63
64
|
this.noGit = Boolean(opts.noGit);
|
|
64
65
|
}
|
|
@@ -140,6 +141,22 @@ export class Repo {
|
|
|
140
141
|
this.walks.set(relDir || ".", out);
|
|
141
142
|
return out;
|
|
142
143
|
}
|
|
144
|
+
// Per-unit scans use the discovery result, never filesystem heuristics.
|
|
145
|
+
// Keep walk() repository-wide for discovery, contracts and authored inputs.
|
|
146
|
+
ownsUnitPath(unitRoot, rel) {
|
|
147
|
+
const normalize = (p) => path.posix.normalize(p || ".").replace(/^\.$/, "");
|
|
148
|
+
const root = normalize(unitRoot);
|
|
149
|
+
const target = normalize(rel);
|
|
150
|
+
if (root && target !== root && !target.startsWith(root + "/")) return false;
|
|
151
|
+
for (const child of this.nestedUnitRoots.get(root) || []) {
|
|
152
|
+
if (target === child || target.startsWith(child + "/")) return false;
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
walkUnit(unitRoot, relDir = unitRoot, { exclude = () => false } = {}) {
|
|
157
|
+
if (!this.ownsUnitPath(unitRoot, relDir)) return [];
|
|
158
|
+
return this.walk(relDir, { exclude: (rel, ent) => !this.ownsUnitPath(unitRoot, rel) || exclude(rel, ent) });
|
|
159
|
+
}
|
|
143
160
|
// Files under a path or a path *prefix*. A contract is often named by a module
|
|
144
161
|
// prefix rather than a single file (`events.go`, `events/*.go`), and
|
|
145
162
|
// requiring an exact directory would reject a perfectly good contract.
|
|
@@ -175,16 +192,17 @@ export class Repo {
|
|
|
175
192
|
return names;
|
|
176
193
|
}
|
|
177
194
|
|
|
178
|
-
git(args) {
|
|
195
|
+
git(args, { trim = true } = {}) {
|
|
179
196
|
// stderr must be piped, not inherited: every caller treats a non-zero exit
|
|
180
197
|
// as a signal to degrade (unborn HEAD, detached worktree, missing remote),
|
|
181
198
|
// and execFileSync would otherwise print git's own "fatal:" line to the
|
|
182
199
|
// user's terminal while the command reports success.
|
|
183
|
-
|
|
200
|
+
const out = execFileSync("git", args, {
|
|
184
201
|
cwd: this.root,
|
|
185
202
|
encoding: "utf8",
|
|
186
203
|
stdio: ["ignore", "pipe", "pipe"],
|
|
187
|
-
})
|
|
204
|
+
});
|
|
205
|
+
return trim ? out.trim() : out;
|
|
188
206
|
}
|
|
189
207
|
hasGit() {
|
|
190
208
|
if (this.noGit) return false;
|
|
@@ -239,7 +257,12 @@ export class Repo {
|
|
|
239
257
|
if (this.noGit || !this.hasGit()) return [];
|
|
240
258
|
let raw;
|
|
241
259
|
try {
|
|
242
|
-
|
|
260
|
+
// Untrimmed: a porcelain record is `XY <path>` with fixed columns, and an
|
|
261
|
+
// unstaged change has X = " ". Trimming would eat that space from the
|
|
262
|
+
// first record, so `.slice(3)` would drop the path's first character —
|
|
263
|
+
// ` M .agentdoc/graph.json` became `agentdoc/graph.json`, slipped past
|
|
264
|
+
// the output exclusion and counted as a dirty input (issue #11).
|
|
265
|
+
raw = this.git(["status", "--porcelain", "-z", "--untracked-files=all"], { trim: false });
|
|
243
266
|
} catch {
|
|
244
267
|
return [];
|
|
245
268
|
}
|