@pareto4/pp-core 1.18.0 → 1.19.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/.claude-plugin/plugin.json +1 -1
- package/agents/pp-code-fixer.md +9 -5
- package/agents/pp-code-reviewer.md +1 -0
- package/agents/pp-codebase-mapper.md +3 -0
- package/agents/pp-debugger.md +3 -2
- package/agents/pp-executor.md +16 -7
- package/agents/pp-integration-checker.md +1 -0
- package/agents/pp-phase-researcher.md +3 -1
- package/agents/pp-plan-checker.md +3 -0
- package/agents/pp-planner.md +5 -1
- package/agents/pp-research-synthesizer.md +1 -1
- package/agents/pp-ui-auditor.md +1 -0
- package/agents/pp-ui-researcher.md +3 -1
- package/agents/pp-verifier.md +10 -2
- package/commands/pp-add-tests.md +1 -1
- package/commands/pp-debug.md +1 -0
- package/commands/pp-init.md +4 -3
- package/commands/pp-quick.md +3 -0
- package/commands/pp-review-backlog.md +2 -1
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/pp-core/bin/lib/capability-registry.cjs +2 -2
- package/pp-core/bin/lib/check-command-router.cjs +60 -20
- package/pp-core/bin/lib/commands.cjs +79 -4
- package/pp-core/bin/lib/compose-bindings.cjs +97 -14
- package/pp-core/bin/lib/discovery-rollup.cjs +9 -6
- package/pp-core/bin/lib/discovery-screens.cjs +20 -2
- package/pp-core/bin/lib/discovery-sweep.cjs +12 -3
- package/pp-core/bin/lib/discovery-vault-check.cjs +37 -3
- package/pp-core/bin/lib/init-contract.cjs +48 -7
- package/pp-core/bin/lib/vault-coordinates.cjs +1 -1
- package/pp-core/bin/lib/vault-index.cjs +18 -5
- package/pp-core/bin/lib/vault-update.cjs +4 -0
- package/pp-core/bin/lib/vault.cjs +2 -2
- package/pp-core/bin/pp-tools.cjs +10 -4
- package/pp-core/org-profile.json +1 -1
- package/pp-core/references/autonomous-best-effort.md +3 -2
- package/pp-core/references/autonomous-lifecycle.md +1 -0
- package/pp-core/references/autonomous-smart-discuss.md +5 -1
- package/pp-core/references/discovery-id-conventions.md +35 -2
- package/pp-core/references/execute-mvp-tdd.md +3 -3
- package/pp-core/references/git-integration.md +57 -33
- package/pp-core/references/git-planning-commit.md +31 -9
- package/pp-core/references/phase-argument-parsing.md +1 -0
- package/pp-core/references/planner-gap-closure.md +1 -0
- package/pp-core/references/planner-revision.md +3 -1
- package/pp-core/references/planner-tdd.md +4 -4
- package/pp-core/references/planning-config.md +3 -3
- package/pp-core/references/shell-portability.md +28 -0
- package/pp-core/references/tdd.md +20 -16
- package/pp-core/references/worktree-path-safety.md +1 -0
- package/pp-core/templates/milestone.md +5 -5
- package/pp-core/workflows/add-backlog.md +1 -1
- package/pp-core/workflows/add-tests.md +5 -2
- package/pp-core/workflows/add-todo.md +3 -1
- package/pp-core/workflows/ai-integration-phase.md +2 -1
- package/pp-core/workflows/audit-fix.md +4 -3
- package/pp-core/workflows/audit-milestone.md +1 -0
- package/pp-core/workflows/autonomous.md +6 -0
- package/pp-core/workflows/check-todos.md +2 -1
- package/pp-core/workflows/cleanup.md +3 -1
- package/pp-core/workflows/code-review-fix.md +12 -5
- package/pp-core/workflows/code-review.md +15 -1
- package/pp-core/workflows/complete-milestone.md +13 -3
- package/pp-core/workflows/debug.md +2 -0
- package/pp-core/workflows/diagnose-issues.md +1 -1
- package/pp-core/workflows/discovery-sprint/session-13-gates.md +1 -1
- package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +51 -7
- package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +5 -3
- package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +10 -8
- package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +11 -2
- package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +5 -5
- package/pp-core/workflows/discovery-sprint/sessions/06-security.md +3 -3
- package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +2 -2
- package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +14 -2
- package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +2 -2
- package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +10 -10
- package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +11 -8
- package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +21 -18
- package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +5 -3
- package/pp-core/workflows/discovery-sprint/templates/context-template.md +9 -3
- package/pp-core/workflows/discovery-sprint/vault-context.md +14 -3
- package/pp-core/workflows/discovery-sprint.md +11 -8
- package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +1 -0
- package/pp-core/workflows/discuss-phase-assumptions.md +5 -3
- package/pp-core/workflows/discuss-phase.md +8 -3
- package/pp-core/workflows/docs-update.md +1 -0
- package/pp-core/workflows/eval-review.md +2 -1
- package/pp-core/workflows/execute-phase/steps/check-query-gate.md +7 -6
- package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +1 -0
- package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -0
- package/pp-core/workflows/execute-phase/steps/vault-fetch.md +12 -0
- package/pp-core/workflows/execute-phase.md +19 -7
- package/pp-core/workflows/execute-plan.md +18 -7
- package/pp-core/workflows/explore.md +1 -1
- package/pp-core/workflows/forensics.md +4 -2
- package/pp-core/workflows/health.md +1 -0
- package/pp-core/workflows/import.md +2 -1
- package/pp-core/workflows/ingest-docs.md +1 -0
- package/pp-core/workflows/init/steps/client-context-capture.md +1 -0
- package/pp-core/workflows/init/steps/github-provisioning.md +145 -0
- package/pp-core/workflows/init/steps/gitlab-provisioning.md +18 -4
- package/pp-core/workflows/init/steps/reachability-outcome.md +3 -1
- package/pp-core/workflows/init.md +56 -66
- package/pp-core/workflows/map-codebase.md +5 -1
- package/pp-core/workflows/milestone-summary.md +2 -1
- package/pp-core/workflows/mvp-phase.md +1 -0
- package/pp-core/workflows/new-milestone.md +9 -5
- package/pp-core/workflows/new-project.md +9 -8
- package/pp-core/workflows/new-workspace.md +1 -0
- package/pp-core/workflows/next.md +2 -1
- package/pp-core/workflows/pause-work.md +6 -4
- package/pp-core/workflows/plan-milestone-gaps.md +3 -1
- package/pp-core/workflows/plan-phase/steps/chunked-planning.md +1 -1
- package/pp-core/workflows/plan-phase.md +21 -6
- package/pp-core/workflows/plan-review-convergence.md +2 -0
- package/pp-core/workflows/plant-seed.md +4 -2
- package/pp-core/workflows/pr-branch.md +2 -0
- package/pp-core/workflows/progress.md +5 -0
- package/pp-core/workflows/quick.md +11 -4
- package/pp-core/workflows/reapply-patches.md +3 -0
- package/pp-core/workflows/remove-phase.md +1 -1
- package/pp-core/workflows/resume-project.md +3 -0
- package/pp-core/workflows/review.md +7 -1
- package/pp-core/workflows/secure-phase.md +2 -1
- package/pp-core/workflows/session-report.md +1 -0
- package/pp-core/workflows/ship.md +8 -1
- package/pp-core/workflows/sketch-wrap-up.md +1 -1
- package/pp-core/workflows/sketch.md +3 -2
- package/pp-core/workflows/spec-phase.md +2 -1
- package/pp-core/workflows/spike-wrap-up.md +1 -1
- package/pp-core/workflows/spike.md +4 -3
- package/pp-core/workflows/sync-skills.md +5 -2
- package/pp-core/workflows/thread.md +3 -2
- package/pp-core/workflows/transition.md +6 -0
- package/pp-core/workflows/ui-phase.md +3 -1
- package/pp-core/workflows/ui-review.md +2 -1
- package/pp-core/workflows/ultraplan-phase.md +1 -0
- package/pp-core/workflows/undo.md +14 -10
- package/pp-core/workflows/update.md +5 -0
- package/pp-core/workflows/validate-phase.md +6 -3
- package/pp-core/workflows/vault-review.md +2 -1
- package/pp-core/workflows/vault-write-back.md +5 -0
- package/pp-core/workflows/verify-phase.md +5 -0
- package/pp-core/workflows/verify-work.md +8 -1
- package/scripts/prompt-injection-scan.sh +1 -1
- package/scripts/sync-runtime-launcher.cjs +115 -5
|
@@ -771,6 +771,57 @@ function cmdUiSafetyGate(projectDir, args, raw) {
|
|
|
771
771
|
}
|
|
772
772
|
output(computeUiSafetyGate(projectDir, phase), raw, undefined);
|
|
773
773
|
}
|
|
774
|
+
/** Subject types the TDD review gate looks for. */
|
|
775
|
+
const TDD_GATE_TYPES = new Set(['test', 'feat', 'refactor']);
|
|
776
|
+
/**
|
|
777
|
+
* Parse one commit message into the TDD gate keys it satisfies, each
|
|
778
|
+
* `<type>\0<plan id>`. The type comes from the Conventional Commit subject
|
|
779
|
+
* (`test(auth): …`, `test: …`, `test!: …`); the plan id from every
|
|
780
|
+
* `Phase: <id>` line in the message's last paragraph (the git trailer block).
|
|
781
|
+
* A subject whose scope IS the plan id (`test(01-02): …`, the convention
|
|
782
|
+
* before the trailer) yields that id too, so older history still counts.
|
|
783
|
+
*/
|
|
784
|
+
function tddGateKeys(message) {
|
|
785
|
+
const text = message.replace(/\r\n/g, '\n').trim();
|
|
786
|
+
const subject = text.split('\n', 1)[0] || '';
|
|
787
|
+
const m = /^([a-z]+)(?:\(([^)]*)\))?!?:/.exec(subject);
|
|
788
|
+
if (!m || !TDD_GATE_TYPES.has(m[1]))
|
|
789
|
+
return [];
|
|
790
|
+
const type = m[1];
|
|
791
|
+
const ids = new Set();
|
|
792
|
+
const paragraphs = text.split(/\n[ \t]*\n/);
|
|
793
|
+
if (paragraphs.length > 1) {
|
|
794
|
+
for (const line of paragraphs[paragraphs.length - 1].split('\n')) {
|
|
795
|
+
const t = /^Phase:[ \t]*(\S+)[ \t]*$/.exec(line);
|
|
796
|
+
if (t)
|
|
797
|
+
ids.add(t[1]);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
// Legacy: a scope shaped like a plan id (starts with a digit), never a codebase noun.
|
|
801
|
+
if (m[2] && /^\d[\w.-]*$/.test(m[2].trim()))
|
|
802
|
+
ids.add(m[2].trim());
|
|
803
|
+
return [...ids].map((id) => `${type}\0${id}`);
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Every TDD gate key satisfied by a commit that touches the working tree
|
|
807
|
+
* (`-- .`, as before), read in one `git log`. An unreadable history (no git,
|
|
808
|
+
* no commits) yields the empty set, so every gate reads as missing.
|
|
809
|
+
*/
|
|
810
|
+
function readTddGateCommits(projectDir) {
|
|
811
|
+
const keys = new Set();
|
|
812
|
+
let log = '';
|
|
813
|
+
try {
|
|
814
|
+
log = (0, node_child_process_1.execFileSync)('git', ['log', '--format=%x1e%B', '--', '.'], {
|
|
815
|
+
cwd: projectDir, encoding: 'utf-8', maxBuffer: 16 * 1024 * 1024, windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'],
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
catch { /* git unavailable or no commits */ }
|
|
819
|
+
for (const record of log.split('\x1e')) {
|
|
820
|
+
for (const key of tddGateKeys(record))
|
|
821
|
+
keys.add(key);
|
|
822
|
+
}
|
|
823
|
+
return keys;
|
|
824
|
+
}
|
|
774
825
|
function cmdTddReviewCheckpoint(projectDir, args, raw) {
|
|
775
826
|
// args[0] = 'check', args[1] = 'tdd-review-checkpoint' (normalized), args[2] = phase
|
|
776
827
|
const phase = args[2] || '';
|
|
@@ -830,31 +881,19 @@ function cmdTddReviewCheckpoint(projectDir, args, raw) {
|
|
|
830
881
|
return;
|
|
831
882
|
}
|
|
832
883
|
// For each TDD plan, extract the plan ID (padded plan number) and check git log
|
|
884
|
+
const gateCommits = readTddGateCommits(projectDir);
|
|
833
885
|
const rows = [];
|
|
834
886
|
for (const planPath of tddPlanFiles) {
|
|
835
887
|
// Extract plan ID from filename (e.g. "01-02-PLAN.md" → "01-02", or "03-PLAN.md" → "03")
|
|
836
888
|
const basename = node_path_1.default.basename(planPath, '-PLAN.md');
|
|
837
889
|
// planId for commit grep: phase-plan format, e.g. "01-02"
|
|
838
890
|
const planId = basename;
|
|
839
|
-
//
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
red = redCommit.trim().length > 0;
|
|
846
|
-
}
|
|
847
|
-
catch { /* git unavailable or no match */ }
|
|
848
|
-
try {
|
|
849
|
-
const greenCommit = (0, node_child_process_1.execFileSync)('git', ['log', '--oneline', `--grep=^feat(${planId}):`, '--', '.'], { cwd: projectDir, encoding: 'utf-8', maxBuffer: 1024 * 1024, windowsHide: true });
|
|
850
|
-
green = greenCommit.trim().length > 0;
|
|
851
|
-
}
|
|
852
|
-
catch { /* git unavailable or no match */ }
|
|
853
|
-
try {
|
|
854
|
-
const refactorCommit = (0, node_child_process_1.execFileSync)('git', ['log', '--oneline', `--grep=^refactor(${planId}):`, '--', '.'], { cwd: projectDir, encoding: 'utf-8', maxBuffer: 1024 * 1024, windowsHide: true });
|
|
855
|
-
refactor = refactorCommit.trim().length > 0;
|
|
856
|
-
}
|
|
857
|
-
catch { /* git unavailable or no match */ }
|
|
891
|
+
// Gate commits are found by type and the `Phase: {planId}` trailer — the
|
|
892
|
+
// scope names a part of the codebase, never the plan. A commit made before
|
|
893
|
+
// the trailer convention (`test({planId}): …`) still counts.
|
|
894
|
+
const red = gateCommits.has(`test\0${planId}`);
|
|
895
|
+
const green = gateCommits.has(`feat\0${planId}`);
|
|
896
|
+
const refactor = gateCommits.has(`refactor\0${planId}`);
|
|
858
897
|
const missing = [];
|
|
859
898
|
if (!red)
|
|
860
899
|
missing.push('RED');
|
|
@@ -884,7 +923,7 @@ function cmdTddReviewCheckpoint(projectDir, args, raw) {
|
|
|
884
923
|
table += '\n\n⚠ Gate violations are advisory — review before advancing.';
|
|
885
924
|
for (const r of rows.filter(row => row.status === 'FAIL')) {
|
|
886
925
|
table += `\n Plan ${r.planId} missing: ${r.missing.join(', ')} gate commit(s).`;
|
|
887
|
-
table += `\n Expected
|
|
926
|
+
table += `\n Expected commits: test(<area>): ... → feat(<area>): ..., each with the trailer Phase: ${r.planId}`;
|
|
888
927
|
}
|
|
889
928
|
}
|
|
890
929
|
const result = {
|
|
@@ -3154,6 +3193,7 @@ module.exports = {
|
|
|
3154
3193
|
computeUiSafetyGate,
|
|
3155
3194
|
cmdGapAnalysisPlanPost,
|
|
3156
3195
|
cmdTddReviewCheckpoint,
|
|
3196
|
+
tddGateKeys,
|
|
3157
3197
|
cmdShipPlatformDrift,
|
|
3158
3198
|
cmdVaultStatus,
|
|
3159
3199
|
// Exported so tests HARVEST the volatility sentence rather than restating its
|
|
@@ -14,7 +14,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
14
14
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
15
15
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
16
16
|
const ioMod = require("./io.cjs");
|
|
17
|
-
const { output, error } = ioMod;
|
|
17
|
+
const { output, error, ERROR_REASON } = ioMod;
|
|
18
18
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
19
19
|
const configLoaderMod = require("./config-loader.cjs");
|
|
20
20
|
const { loadConfig, isGitIgnored } = configLoaderMod;
|
|
@@ -423,10 +423,76 @@ function cmdEffortSync(cwd, raw, opts) {
|
|
|
423
423
|
}
|
|
424
424
|
output({ synced, skipped, changes, dry_run: dryRun, agents_dir: agentsDir }, raw, synced > 0 ? 'changed' : 'ok');
|
|
425
425
|
}
|
|
426
|
-
|
|
426
|
+
// ─── Commit trailers (Phase: / Quick:) ────────────────────────────────────────
|
|
427
|
+
//
|
|
428
|
+
// The Conventional Commit scope names a part of the codebase (`docs(planning)`,
|
|
429
|
+
// `feat(auth)`), never a phase or plan number. The phase, plan or quick-task
|
|
430
|
+
// identity a commit belongs to travels as ONE git trailer instead —
|
|
431
|
+
// `Phase: 02-03`, `Phase: 02` or `Quick: <id>` — so readers find it with
|
|
432
|
+
// `%(trailers:key=Phase)` rather than by parsing the subject.
|
|
433
|
+
/** Loose id shape: digits, letters, dots, dashes, underscores; no whitespace. */
|
|
434
|
+
const COMMIT_TRAILER_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
435
|
+
/**
|
|
436
|
+
* Pull `--phase <id>` and `--quick <id>` (and their `--flag=<id>` forms) out of
|
|
437
|
+
* a `commit` / `commit-to-subrepo` argv, so neither the value nor the flag is
|
|
438
|
+
* taken for part of the message or for a `--files` path. A flag with no value
|
|
439
|
+
* yields the empty string, which `buildCommitTrailer` rejects.
|
|
440
|
+
*/
|
|
441
|
+
function extractCommitTrailerArgs(args) {
|
|
442
|
+
const rest = [];
|
|
443
|
+
let phase;
|
|
444
|
+
let quick;
|
|
445
|
+
for (let i = 0; i < args.length; i++) {
|
|
446
|
+
const a = args[i];
|
|
447
|
+
const eq = /^--(phase|quick)=(.*)$/s.exec(a);
|
|
448
|
+
if (eq) {
|
|
449
|
+
if (eq[1] === 'phase')
|
|
450
|
+
phase = eq[2];
|
|
451
|
+
else
|
|
452
|
+
quick = eq[2];
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
if (a === '--phase' || a === '--quick') {
|
|
456
|
+
const next = args[i + 1];
|
|
457
|
+
const hasValue = next !== undefined && !next.startsWith('--');
|
|
458
|
+
const value = hasValue ? next : '';
|
|
459
|
+
if (hasValue)
|
|
460
|
+
i++;
|
|
461
|
+
if (a === '--phase')
|
|
462
|
+
phase = value;
|
|
463
|
+
else
|
|
464
|
+
quick = value;
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
rest.push(a);
|
|
468
|
+
}
|
|
469
|
+
return { args: rest, phase, quick };
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* The trailer paragraph for a commit, or null when neither id is given.
|
|
473
|
+
* Both ids may be given; they share one paragraph so git reads both as
|
|
474
|
+
* trailers. Errors (exit 1, reason `usage`) on an id that is empty or holds
|
|
475
|
+
* anything but letters, digits, dots, dashes or underscores — a newline in
|
|
476
|
+
* an id would otherwise forge further trailers.
|
|
477
|
+
*/
|
|
478
|
+
function buildCommitTrailer(opts) {
|
|
479
|
+
const lines = [];
|
|
480
|
+
for (const [key, flag, value] of [['Phase', '--phase', opts.phase], ['Quick', '--quick', opts.quick]]) {
|
|
481
|
+
if (value === undefined)
|
|
482
|
+
continue;
|
|
483
|
+
if (!COMMIT_TRAILER_ID_RE.test(value)) {
|
|
484
|
+
error(`${flag} must be a phase/plan or quick-task id such as 02 or 02-03 (letters, digits, dots, dashes; got ${JSON.stringify(value)})`, ERROR_REASON.USAGE);
|
|
485
|
+
}
|
|
486
|
+
lines.push(`${key}: ${value}`);
|
|
487
|
+
}
|
|
488
|
+
return lines.length > 0 ? lines.join('\n') : null;
|
|
489
|
+
}
|
|
490
|
+
function cmdCommit(cwd, message, files, raw, amend, noVerify, trailerIds = {}) {
|
|
427
491
|
if (!message && !amend) {
|
|
428
492
|
error('commit message required');
|
|
429
493
|
}
|
|
494
|
+
// --amend keeps the previous message (--no-edit), so a trailer is ignored there.
|
|
495
|
+
const trailer = amend ? null : buildCommitTrailer(trailerIds);
|
|
430
496
|
// Sanitize commit message: strip invisible chars and injection markers
|
|
431
497
|
// that could hijack agent context when commit messages are read back
|
|
432
498
|
let sanitizedMessage = message;
|
|
@@ -513,6 +579,9 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify) {
|
|
|
513
579
|
}
|
|
514
580
|
// Commit (--no-verify skips pre-commit hooks, used by parallel executor agents)
|
|
515
581
|
const commitArgs = amend ? ['commit', '--amend', '--no-edit'] : ['commit', '-m', sanitizedMessage];
|
|
582
|
+
// A second -m is its own paragraph, which git reads as the trailer block.
|
|
583
|
+
if (trailer)
|
|
584
|
+
commitArgs.push('-m', trailer);
|
|
516
585
|
if (noVerify)
|
|
517
586
|
commitArgs.push('--no-verify');
|
|
518
587
|
const commitResult = (0, shell_command_projection_cjs_1.execGit)(commitArgs, { cwd });
|
|
@@ -591,10 +660,11 @@ function groupFilesBySubrepo(files, subRepos) {
|
|
|
591
660
|
}
|
|
592
661
|
return { grouped, unmatched };
|
|
593
662
|
}
|
|
594
|
-
function cmdCommitToSubrepo(cwd, message, files, raw) {
|
|
663
|
+
function cmdCommitToSubrepo(cwd, message, files, raw, trailerIds = {}) {
|
|
595
664
|
if (!message) {
|
|
596
665
|
error('commit message required');
|
|
597
666
|
}
|
|
667
|
+
const trailer = buildCommitTrailer(trailerIds);
|
|
598
668
|
const config = loadConfig(cwd);
|
|
599
669
|
const subRepos = config['sub_repos'];
|
|
600
670
|
if (!subRepos || subRepos.length === 0) {
|
|
@@ -617,7 +687,10 @@ function cmdCommitToSubrepo(cwd, message, files, raw) {
|
|
|
617
687
|
(0, shell_command_projection_cjs_1.execGit)(['add', relativePath], { cwd: repoCwd });
|
|
618
688
|
}
|
|
619
689
|
// Commit
|
|
620
|
-
const
|
|
690
|
+
const commitArgs = ['commit', '-m', message];
|
|
691
|
+
if (trailer)
|
|
692
|
+
commitArgs.push('-m', trailer);
|
|
693
|
+
const commitResult = (0, shell_command_projection_cjs_1.execGit)(commitArgs, { cwd: repoCwd });
|
|
621
694
|
if (commitResult.exitCode !== 0) {
|
|
622
695
|
if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
|
|
623
696
|
repos[repo] = { committed: false, hash: null, files: repoFiles, reason: 'nothing_to_commit' };
|
|
@@ -1252,6 +1325,8 @@ module.exports = {
|
|
|
1252
1325
|
cmdEffortSync,
|
|
1253
1326
|
cmdCommit,
|
|
1254
1327
|
cmdCommitToSubrepo,
|
|
1328
|
+
extractCommitTrailerArgs,
|
|
1329
|
+
buildCommitTrailer,
|
|
1255
1330
|
cmdSummaryExtract,
|
|
1256
1331
|
cmdWebsearch,
|
|
1257
1332
|
cmdProgressRender,
|
|
@@ -54,6 +54,8 @@ exports.resolveLatestVaultTag = resolveLatestVaultTag;
|
|
|
54
54
|
exports.resolvePlatformRepoUrl = resolvePlatformRepoUrl;
|
|
55
55
|
exports.resolveLatestPlatformTag = resolveLatestPlatformTag;
|
|
56
56
|
exports.resolveCreatedWithFrom = resolveCreatedWithFrom;
|
|
57
|
+
exports.clientVaultChoice = clientVaultChoice;
|
|
58
|
+
exports.validateClientVaultAnswer = validateClientVaultAnswer;
|
|
57
59
|
exports.composeBindings = composeBindings;
|
|
58
60
|
exports.redactComposeResultForEmission = redactComposeResultForEmission;
|
|
59
61
|
exports.cmdComposeBindings = cmdComposeBindings;
|
|
@@ -579,7 +581,7 @@ function resolveCreatedWith() {
|
|
|
579
581
|
* string. Every emitted field is one the schema's `additionalProperties:
|
|
580
582
|
* false` property sets already allow.
|
|
581
583
|
*/
|
|
582
|
-
function composeCandidate(cwd, seed, provider, opts) {
|
|
584
|
+
function composeCandidate(cwd, seed, provider, opts, existingClient) {
|
|
583
585
|
const candidate = {};
|
|
584
586
|
const provenance = {};
|
|
585
587
|
const warnings = [];
|
|
@@ -614,23 +616,37 @@ function composeCandidate(cwd, seed, provider, opts) {
|
|
|
614
616
|
// client.name / client.slug (project-answer)
|
|
615
617
|
setField('client.name', cfgGet(cwd, 'project.client_name', undefined), 'project-answer');
|
|
616
618
|
setField('client.slug', cfgGet(cwd, 'project.client_slug', undefined), 'project-answer');
|
|
617
|
-
// client.vaultRepo
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
if (
|
|
619
|
+
// client.vaultRepo (project-answer or profile-seed). The seeded ref and
|
|
620
|
+
// host describe the seeded repo, so they are proposed only while that repo
|
|
621
|
+
// is the one the binding will name.
|
|
622
|
+
const answerRepo = isUsableString(opts.clientVaultRepo) ? opts.clientVaultRepo.trim() : undefined;
|
|
623
|
+
const boundRepo = existingClient.vaultRepo ?? answerRepo ?? seed.clientVaultRepo;
|
|
624
|
+
const seedIsBound = answerRepo === undefined && boundRepo === seed.clientVaultRepo;
|
|
625
|
+
if (answerRepo !== undefined) {
|
|
626
|
+
setField('client.vaultRepo', answerRepo, 'project-answer');
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
setField('client.vaultRepo', seed.clientVaultRepo, 'profile-seed');
|
|
630
|
+
}
|
|
631
|
+
if (seedIsBound) {
|
|
632
|
+
setField('client.vaultRef', seed.clientVaultRef, 'profile-seed');
|
|
633
|
+
setField('client.vaultHost', seed.clientVaultHost, 'profile-seed');
|
|
634
|
+
}
|
|
635
|
+
// client.vaultRef (resolved-tag) — FORK-ONLY (PBI 74316). When the bound
|
|
636
|
+
// vault repo has no ref yet, propose that repo's latest stable tag.
|
|
637
|
+
const boundRef = existingClient.vaultRef ?? (seedIsBound ? seed.clientVaultRef : undefined);
|
|
638
|
+
if (isUsableString(boundRepo) && !isUsableString(boundRef)) {
|
|
624
639
|
if (opts.offline === true) {
|
|
625
640
|
pushDeduped(warnings, [
|
|
626
641
|
bound('client.vaultRef was not proposed (offline) — the vault resolves "main" until the project is pinned with "pp-tools vault update <tag>".'),
|
|
627
642
|
]);
|
|
628
643
|
}
|
|
629
644
|
else {
|
|
630
|
-
const vaultHost =
|
|
631
|
-
? seed.clientVaultHost
|
|
632
|
-
|
|
633
|
-
|
|
645
|
+
const vaultHost = existingClient.vaultHost ??
|
|
646
|
+
(seedIsBound && isUsableString(seed.clientVaultHost) ? seed.clientVaultHost : undefined) ??
|
|
647
|
+
hostRes.host ??
|
|
648
|
+
(0, vault_coordinates_cjs_1.defaultHostForProvider)(provider);
|
|
649
|
+
const vaultTag = resolveLatestVaultTag(boundRepo, vaultHost, provider, opts);
|
|
634
650
|
pushDeduped(warnings, vaultTag.warnings);
|
|
635
651
|
if (vaultTag.tag) {
|
|
636
652
|
setField('client.vaultRef', vaultTag.tag, 'resolved-tag');
|
|
@@ -746,6 +762,73 @@ function readExistingBindings(cwd) {
|
|
|
746
762
|
};
|
|
747
763
|
}
|
|
748
764
|
}
|
|
765
|
+
/** Read the usable `client.vault*` strings of an existing binding; the merge
|
|
766
|
+
* keeps them, so the ref proposal must follow the repo they name. */
|
|
767
|
+
function existingVaultCoordinates(data) {
|
|
768
|
+
const client = data.client;
|
|
769
|
+
if (!isObject(client))
|
|
770
|
+
return {};
|
|
771
|
+
const out = {};
|
|
772
|
+
if (isUsableString(client.vaultRepo))
|
|
773
|
+
out.vaultRepo = client.vaultRepo;
|
|
774
|
+
if (isUsableString(client.vaultRef))
|
|
775
|
+
out.vaultRef = client.vaultRef;
|
|
776
|
+
if (isUsableString(client.vaultHost))
|
|
777
|
+
out.vaultHost = client.vaultHost;
|
|
778
|
+
return out;
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Whether `/pp-init` asks for a client vault: whenever the project has none
|
|
782
|
+
* bound yet and a binding can be composed at all. Without a `platform.repo`
|
|
783
|
+
* from the org profile or an existing binding nothing is written (INIT-02),
|
|
784
|
+
* so an answer would be dropped silently. `defaultRepo` is the org
|
|
785
|
+
* profile's seeded vault a blank answer keeps, display-safe
|
|
786
|
+
* (`redactCoordinateForEmission`), or `null` when nothing is seeded and a
|
|
787
|
+
* blank answer binds no client vault. Never throws.
|
|
788
|
+
*/
|
|
789
|
+
function clientVaultChoice(cwd, opts = {}) {
|
|
790
|
+
try {
|
|
791
|
+
const seed = readSeed(cwd, opts).values;
|
|
792
|
+
const seedRepo = seed.clientVaultRepo;
|
|
793
|
+
const existing = readExistingBindings(cwd);
|
|
794
|
+
const existingPlatform = existing.data.platform;
|
|
795
|
+
const composable = isUsableString(seed.platformRepo) || (isObject(existingPlatform) && isUsableString(existingPlatform.repo));
|
|
796
|
+
const offered = composable && !existing.malformed && existingVaultCoordinates(existing.data).vaultRepo === undefined;
|
|
797
|
+
return { offered, defaultRepo: offered && isUsableString(seedRepo) ? String((0, platform_bindings_cjs_1.redactCoordinateForEmission)(seedRepo)) : null };
|
|
798
|
+
}
|
|
799
|
+
catch {
|
|
800
|
+
return { offered: false, defaultRepo: null };
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
// ─── validateClientVaultAnswer ────────────────────────────────────────────────
|
|
804
|
+
/**
|
|
805
|
+
* Validate the `/pp-init` client-vault answer. Blank or `none` means "keep
|
|
806
|
+
* the org profile's seeded vault, if any" (`value: null`). Anything else must pass the
|
|
807
|
+
* seed scalar guard, `validateBindings`' own `client.vaultRepo` rule without
|
|
808
|
+
* its machine-specific warning, and carry nothing the emission redactor would
|
|
809
|
+
* hide, so this adds no second pattern for the field.
|
|
810
|
+
*/
|
|
811
|
+
function validateClientVaultAnswer(answer) {
|
|
812
|
+
if (typeof answer !== 'string' || answer.trim() === '' || answer.trim().toLowerCase() === 'none') {
|
|
813
|
+
return { ok: true, value: null };
|
|
814
|
+
}
|
|
815
|
+
const scalar = dispositionSeedValue(`${org_profile_cjs_1.ORG_PROFILE_SEED_SECTION}.client.vaultRef`, answer);
|
|
816
|
+
if (!scalar.ok)
|
|
817
|
+
return { ok: false, reason: scalar.reason };
|
|
818
|
+
const probe = (0, platform_bindings_cjs_1.validateBindings)({
|
|
819
|
+
schemaVersion: platform_bindings_cjs_1.CONTRACT_SCHEMA_VERSION,
|
|
820
|
+
platform: { repo: 'placeholder/placeholder' },
|
|
821
|
+
client: { vaultRepo: scalar.value },
|
|
822
|
+
});
|
|
823
|
+
// The binding is committed: a machine-specific path resolves nowhere else, and a credential would be published.
|
|
824
|
+
const vaultIssues = [...probe.errors, ...probe.warnings].filter((e) => leadingToken(e).startsWith('client.vaultRepo'));
|
|
825
|
+
if (vaultIssues.length > 0)
|
|
826
|
+
return { ok: false, reason: vaultIssues.join('; ') };
|
|
827
|
+
if ((0, platform_bindings_cjs_1.redactCoordinateForEmission)(scalar.value) !== scalar.value) {
|
|
828
|
+
return { ok: false, reason: 'a vault repo must not carry credentials — the binding is committed; use a credential helper' };
|
|
829
|
+
}
|
|
830
|
+
return { ok: true, value: scalar.value };
|
|
831
|
+
}
|
|
749
832
|
/**
|
|
750
833
|
* D-08's absent-only merge. `merged` starts as a deep clone of `existing` —
|
|
751
834
|
* every field the existing file already carries (including the provisional
|
|
@@ -816,9 +899,9 @@ function composeBindings(cwd, opts = {}) {
|
|
|
816
899
|
pushDeduped(warnings, providerWarnings);
|
|
817
900
|
const seedResult = readSeed(cwd, { _globalRoot: opts._globalRoot });
|
|
818
901
|
pushDeduped(warnings, seedResult.warnings);
|
|
819
|
-
const candidateResult = composeCandidate(cwd, seedResult.values, provider, opts);
|
|
820
|
-
pushDeduped(warnings, candidateResult.warnings);
|
|
821
902
|
const existing = readExistingBindings(cwd);
|
|
903
|
+
const candidateResult = composeCandidate(cwd, seedResult.values, provider, opts, existingVaultCoordinates(existing.data));
|
|
904
|
+
pushDeduped(warnings, candidateResult.warnings);
|
|
822
905
|
pushDeduped(warnings, existing.warnings);
|
|
823
906
|
if (existing.malformed) {
|
|
824
907
|
return {
|
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
* relation suffix (`refines` / `absorbed into`) whose target requirement ID
|
|
13
13
|
* must exist in the same table. Grammar single-source: same reference file.
|
|
14
14
|
*
|
|
15
|
-
* Phase 3 (D-44), extended by Phase 4 (D-50): a Prefix Registry
|
|
16
|
-
* claim one of the
|
|
17
|
-
* — they all match the requirement-ID regex, so
|
|
18
|
-
* the decision / action / sparring /
|
|
19
|
-
*
|
|
15
|
+
* Phase 3 (D-44), extended by Phase 4 (D-50) and issue #12: a Prefix Registry
|
|
16
|
+
* row may not claim one of the seven reserved prefixes (`D-` `A-` `SP-` `DG-`
|
|
17
|
+
* `RS-` `SCR-` `PRC-`) — they all match the requirement-ID regex, so
|
|
18
|
+
* registering one collides with the decision / action / sparring /
|
|
19
|
+
* data-gathering / research / screen / process ID families. Reserved set
|
|
20
|
+
* single-source: same reference file.
|
|
20
21
|
*
|
|
21
22
|
* Fail-loud contract (D-08): any unparseable row, malformed/duplicate ID,
|
|
22
23
|
* unknown prefix, out-of-enum MoSCoW/Disposition, or strike/disposition
|
|
@@ -46,7 +47,9 @@ const MOSCOW = Object.freeze({ MUST: 'Must', SHOULD: 'Should', COULD: 'Could', W
|
|
|
46
47
|
// `SCR-<nn>` sequence, so an engagement registering `SCR-` as a domain prefix
|
|
47
48
|
// would make `SCR-01` ambiguous between the screen inventory and the
|
|
48
49
|
// requirements table.
|
|
49
|
-
|
|
50
|
+
// Issue #12 added `PRC-`: Session 2 numbers its Process Inventory `PRC-<nn>`,
|
|
51
|
+
// which a later session cites beside requirement IDs.
|
|
52
|
+
const RESERVED_PREFIXES = new Set(['D-', 'A-', 'SP-', 'DG-', 'RS-', 'SCR-', 'PRC-']);
|
|
50
53
|
const ROW_ID_RE = /^([A-Z][A-Z0-9]*-)(\d+)$/;
|
|
51
54
|
const DROPPED_RE = /^Dropped \((D-\d{2}-\d+)\)$/;
|
|
52
55
|
const SEPARATOR_ROW_RE = /^\s*\|[\s:|-]+\|\s*$/;
|
|
@@ -1287,6 +1287,23 @@ const SCAN_SKIP_DIRS = Object.freeze(new Set([
|
|
|
1287
1287
|
'.next', '.nuxt', '.turbo', '.cache', 'coverage',
|
|
1288
1288
|
'.venv', 'venv', '__pycache__', 'vendor', 'target',
|
|
1289
1289
|
]));
|
|
1290
|
+
/**
|
|
1291
|
+
* True when `dir` holds a pp-core install (`<dir>/pp-core/VERSION`, the marker
|
|
1292
|
+
* `update-context` reads): a project-local runtime directory such as
|
|
1293
|
+
* `.claude` or `.codex`. The walk never enters one. Its engine files, agents
|
|
1294
|
+
* and commands spell the instrumentation literals out, and none of it ships
|
|
1295
|
+
* in the engagement's build, so walking it reports pp-core itself as a leak
|
|
1296
|
+
* (issue #12, item 7). Keyed on the marker rather than on directory names so
|
|
1297
|
+
* every runtime's install directory is covered without a third list of them.
|
|
1298
|
+
*/
|
|
1299
|
+
function isPpCoreInstallDir(dir) {
|
|
1300
|
+
try {
|
|
1301
|
+
return node_fs_1.default.statSync(node_path_1.default.join(dir, 'pp-core', 'VERSION')).isFile();
|
|
1302
|
+
}
|
|
1303
|
+
catch {
|
|
1304
|
+
return false;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1290
1307
|
/**
|
|
1291
1308
|
* The file extensions the scan actually READS. An allowlist rather than a
|
|
1292
1309
|
* denylist, because the bound has to hold for file types nobody has thought of
|
|
@@ -1475,7 +1492,8 @@ function loadScanAllowlist(loaded) {
|
|
|
1475
1492
|
* - every entry is resolved and confirmed strictly beneath the resolved
|
|
1476
1493
|
* project root before any stat or read, the same posture the roll-up
|
|
1477
1494
|
* traversal guard takes (T-04-16);
|
|
1478
|
-
* - `SCAN_SKIP_DIRS` keeps the walk out of the dependency tree
|
|
1495
|
+
* - `SCAN_SKIP_DIRS` keeps the walk out of the dependency tree, and
|
|
1496
|
+
* `isPpCoreInstallDir` out of a project-local pp-core install;
|
|
1479
1497
|
* - `SCAN_EXTENSIONS` bounds what is read;
|
|
1480
1498
|
* - the file and byte caps stop the walk.
|
|
1481
1499
|
*
|
|
@@ -1536,7 +1554,7 @@ function scanBuildTree(cwd, options = {}) {
|
|
|
1536
1554
|
if (stat.isSymbolicLink())
|
|
1537
1555
|
continue;
|
|
1538
1556
|
if (stat.isDirectory()) {
|
|
1539
|
-
if (!SCAN_SKIP_DIRS.has(name))
|
|
1557
|
+
if (!SCAN_SKIP_DIRS.has(name) && !isPpCoreInstallDir(full))
|
|
1540
1558
|
stack.push(full);
|
|
1541
1559
|
continue;
|
|
1542
1560
|
}
|
|
@@ -63,12 +63,21 @@ function parseAttrs(src) {
|
|
|
63
63
|
}
|
|
64
64
|
return attrs;
|
|
65
65
|
}
|
|
66
|
-
|
|
66
|
+
/**
|
|
67
|
+
* A multi-word PascalCase name (`UiButton`, `UiSelect`): the component spelling
|
|
68
|
+
* of a kebab-case custom element (`ui-button`), which a string template keeps
|
|
69
|
+
* and lowercasing would hide (issue #12, item 8). A single word (`Div`) or an
|
|
70
|
+
* all-caps tag (`BUTTON`) is plain HTML.
|
|
71
|
+
*/
|
|
72
|
+
function isPascalComponent(rawTag) {
|
|
73
|
+
return /^[A-Z][a-z0-9]+[A-Z]/.test(rawTag);
|
|
74
|
+
}
|
|
75
|
+
function isFunctional(tag, attrs, rawTag = tag) {
|
|
67
76
|
if (attrs.has('data-req'))
|
|
68
77
|
return true;
|
|
69
78
|
if (tag === 'input' && (attrs.get('type') || '').toLowerCase() === 'hidden')
|
|
70
79
|
return false;
|
|
71
|
-
if (FUNCTIONAL_TAGS.has(tag) || tag.includes('-'))
|
|
80
|
+
if (FUNCTIONAL_TAGS.has(tag) || tag.includes('-') || isPascalComponent(rawTag))
|
|
72
81
|
return true;
|
|
73
82
|
const role = (attrs.get('role') || '').toLowerCase();
|
|
74
83
|
return FUNCTIONAL_ROLES.has(role);
|
|
@@ -107,7 +116,7 @@ function scanPage(page, html, violations) {
|
|
|
107
116
|
const parentScreen = stack.length ? stack[stack.length - 1].screen : null;
|
|
108
117
|
const screen = screenFrom(tag, attrs) ?? parentScreen;
|
|
109
118
|
const line = lineAt(text, m.index);
|
|
110
|
-
if (isFunctional(tag, attrs)) {
|
|
119
|
+
if (isFunctional(tag, attrs, m[2])) {
|
|
111
120
|
const id = attrs.get('id') ?? null;
|
|
112
121
|
let reqs = [];
|
|
113
122
|
if (attrs.has('data-req')) {
|
|
@@ -12,7 +12,11 @@
|
|
|
12
12
|
* - every entry is one well-formed line (`- `<path>` — <why>`, or
|
|
13
13
|
* `- (none) — <why>` when nothing was selected);
|
|
14
14
|
* - every selected document was USED: cited `[vault: <path>]` somewhere in
|
|
15
|
-
* the session notes, or marked `— not used: <reason>` on its own line
|
|
15
|
+
* the session notes, or marked `— not used: <reason>` on its own line;
|
|
16
|
+
* - when the selection used `INDEX.md`, every entry the vault marks
|
|
17
|
+
* `[cross-cutting]` has a record line (issue #12, items 16-17): those rules
|
|
18
|
+
* bind every session, so leaving one out is how an approval route went
|
|
19
|
+
* unseen until Session 12.
|
|
16
20
|
*
|
|
17
21
|
* The selection itself is the facilitator's judgement and is not re-derived
|
|
18
22
|
* here; what is checked is that the record exists, is complete, and that what
|
|
@@ -109,7 +113,7 @@ function parseEntry(n, text) {
|
|
|
109
113
|
* A citation counts only OUTSIDE the section: the record naming a path is not
|
|
110
114
|
* evidence that the path was used.
|
|
111
115
|
*/
|
|
112
|
-
function checkVaultContext(content) {
|
|
116
|
+
function checkVaultContext(content, crossCutting = []) {
|
|
113
117
|
const violations = [];
|
|
114
118
|
const section = extractSection(content);
|
|
115
119
|
if (section === null) {
|
|
@@ -163,8 +167,38 @@ function checkVaultContext(content) {
|
|
|
163
167
|
violations.push({ reason: 'document_unused', line: doc.line, path: doc.path });
|
|
164
168
|
}
|
|
165
169
|
}
|
|
170
|
+
// The cross-cutting entries are owed a line only when the selection read the
|
|
171
|
+
// vault's own index: SUMMARY.md carries no mark, and `none` read nothing.
|
|
172
|
+
if (index === 'INDEX.md') {
|
|
173
|
+
const recorded = new Set(docs.map((d) => d.path));
|
|
174
|
+
for (const p of crossCutting) {
|
|
175
|
+
if (!recorded.has(p))
|
|
176
|
+
violations.push({ reason: 'cross_cutting_missing', path: p });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
166
179
|
return { ok: violations.length === 0, index, documents: docs.length, cited, not_used: notUsed, violations };
|
|
167
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* The paths the cached `INDEX.md` marks `[cross-cutting]`, read through the
|
|
183
|
+
* vault's one index-state function so the bounded read, the withheld filter
|
|
184
|
+
* and the path rule are the engine's own. Withheld entries never reach it;
|
|
185
|
+
* unsafe ones are dropped, since no record line could name them. A vault that
|
|
186
|
+
* is off, uncached or unreadable owes nothing here — `vault status` reports
|
|
187
|
+
* that state.
|
|
188
|
+
*/
|
|
189
|
+
function crossCuttingPaths(cwd) {
|
|
190
|
+
try {
|
|
191
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
192
|
+
const vault = require('./vault.cjs');
|
|
193
|
+
const state = vault.computeVaultIndexState(cwd);
|
|
194
|
+
if (state === null)
|
|
195
|
+
return [];
|
|
196
|
+
return state.entries.filter((e) => e.crossCutting === true && e.resolution !== 'unsafe').map((e) => e.path);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
}
|
|
168
202
|
/** Parse the session argument: a bare integer 1–13, leading zeros allowed (`03` is session 3). */
|
|
169
203
|
function parseSessionArg(raw) {
|
|
170
204
|
if (typeof raw !== 'string' || !/^\d{1,3}$/.test(raw))
|
|
@@ -189,7 +223,7 @@ function checkSessionVaultContext(cwd, n) {
|
|
|
189
223
|
violations: [{ reason: 'session_file_missing' }],
|
|
190
224
|
};
|
|
191
225
|
}
|
|
192
|
-
return { session: n, file: rel, ...checkVaultContext(content) };
|
|
226
|
+
return { session: n, file: rel, ...checkVaultContext(content, crossCuttingPaths(cwd)) };
|
|
193
227
|
}
|
|
194
228
|
function formatViolation(v) {
|
|
195
229
|
const where = v.line !== undefined ? `line ${v.line}: ` : '';
|