backend-skeleton 1.0.0-beta.8 → 1.0.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/README.md +122 -12
- package/bin/bskel.mjs +738 -12
- package/contracts/completeness.mjs +10 -0
- package/contracts/emit.mjs +5 -1
- package/contracts/export.mjs +26 -3
- package/contracts/openapi.mjs +29 -3
- package/handles/_engine.mjs +79 -29
- package/handles/providers/java-spring/plan.mjs +22 -9
- package/handles/providers/java-spring/templates/HandleController.java.tmpl +7 -3
- package/handles/providers/java-spring/templates/ResourceResolver.java.tmpl +12 -6
- package/handles/providers/python-fastapi/templates/record_snapshot.py.tmpl +101 -39
- package/handles/providers/typescript-express/emit.mjs +23 -23
- package/handles/providers/typescript-express/observe.mjs +101 -0
- package/handles/providers/typescript-express/templates/contractCheck.ts.tmpl +136 -0
- package/handles/providers/typescript-express/templates/observeContract.ts.tmpl +146 -0
- package/handles/providers/typescript-express/templates/observedSchema.ts.tmpl +116 -0
- package/lib/attest.mjs +40 -0
- package/lib/cli.mjs +169 -2
- package/lib/cross-feature-collisions.mjs +286 -0
- package/lib/diff.mjs +35 -0
- package/lib/field-dependencies.mjs +355 -0
- package/lib/fsutil.mjs +7 -2
- package/lib/gate-definitions.mjs +117 -1
- package/lib/gates.mjs +5 -1
- package/lib/http-server.mjs +358 -0
- package/lib/lock.mjs +68 -15
- package/lib/patch-kinds.mjs +52 -0
- package/lib/patch-transactions.mjs +206 -0
- package/lib/serve-ui.html +328 -0
- package/lib/workflow.mjs +39 -3
- package/package.json +5 -2
- package/scanners/adapters/java-spring.mjs +6 -0
- package/scanners/adapters/python-fastapi.mjs +9 -1
- package/scanners/adapters/typescript-express.mjs +6 -0
- package/scanners/db/ddl-apply.mjs +253 -0
- package/scanners/db/introspect.mjs +61 -32
- package/scanners/db/migrations.mjs +73 -18
- package/schemas/cross-feature-report.schema.json +66 -0
- package/schemas/cross-feature-resolution.schema.json +28 -0
- package/schemas/field-dependency.schema.json +49 -0
- package/schemas/gate-attestation.schema.json +22 -0
- package/schemas/gate-export.schema.json +58 -0
- package/schemas/patch-transaction.schema.json +182 -0
- package/schemas/scan-report.schema.json +6 -4
- package/schemas/stack-choice.schema.json +12 -1
- package/stack/apply.mjs +4 -1
- package/stack/catalog/ngrok.yml +8 -2
- package/stack/config-apply.mjs +168 -0
package/bin/bskel.mjs
CHANGED
|
@@ -30,7 +30,19 @@ import { buildContract, selectModule, CONTRACT_SCHEMA_VERSION } from '../contrac
|
|
|
30
30
|
import { validateEnvelope, operationPayloadSchema } from '../contracts/validate.mjs';
|
|
31
31
|
import { evaluateResolution, loadResolution, saveResolution, requireWarningCode, warningKey, countByCode } from '../contracts/completeness.mjs';
|
|
32
32
|
import { loadPatchApprovals, savePatchApprovals, approvalKey } from '../lib/patch-approvals.mjs';
|
|
33
|
+
import { proposeTransaction, approveTransaction, applyTransaction, rollbackTransaction, loadTransaction, listTransactions } from '../lib/patch-transactions.mjs';
|
|
34
|
+
import { getPatchKind, replanTransaction, PATCH_KIND_NAMES } from '../lib/patch-kinds.mjs';
|
|
35
|
+
import { generateKeypair, signPayload, verifyPayload } from '../lib/attest.mjs';
|
|
33
36
|
import { loadManifest, saveManifest } from '../lib/handles-manifest.mjs';
|
|
37
|
+
import { createHttpServer } from '../lib/http-server.mjs';
|
|
38
|
+
import {
|
|
39
|
+
resolveClassFile, listDownstreamDependents, DependencyOperationError,
|
|
40
|
+
declareDependency, removeDependency, buildDependencyListReport,
|
|
41
|
+
} from '../lib/field-dependencies.mjs';
|
|
42
|
+
import {
|
|
43
|
+
findCollisions, evaluateCrossFeatureFindings, waiverKey,
|
|
44
|
+
crossFeatureReportPath, loadCrossFeatureReport, loadCrossFeatureResolution, saveCrossFeatureResolution,
|
|
45
|
+
} from '../lib/cross-feature-collisions.mjs';
|
|
34
46
|
import { STACKS as NEW_STACKS, ALL_STACK_PARAMS, stacksAccepting } from '../new/index.mjs';
|
|
35
47
|
import {
|
|
36
48
|
requireSingleLineText, requireValidJavaPackageName, requireValidArtifactId,
|
|
@@ -47,6 +59,8 @@ import { detectBasePackage } from '../handles/providers/java-spring/plan.mjs';
|
|
|
47
59
|
import { emitObserveJavaSpring } from '../handles/providers/java-spring/observe.mjs';
|
|
48
60
|
import { plan as planPythonFastApi } from '../handles/providers/python-fastapi/plan.mjs';
|
|
49
61
|
import { emitObservePythonFastApi } from '../handles/providers/python-fastapi/observe.mjs';
|
|
62
|
+
import { plan as planTypeScriptExpress } from '../handles/providers/typescript-express/plan.mjs';
|
|
63
|
+
import { emitObserveTypeScriptExpress } from '../handles/providers/typescript-express/observe.mjs';
|
|
50
64
|
import { collectGateStatuses, runBuildCheck, checkArtifacts, checkResolverConflicts } from '../lib/verify.mjs';
|
|
51
65
|
import { computeWorkflowState } from '../lib/workflow.mjs';
|
|
52
66
|
import { computeDoctorChecks, WORKFLOWS as DOCTOR_WORKFLOWS } from '../lib/doctor.mjs';
|
|
@@ -65,6 +79,8 @@ function usage() {
|
|
|
65
79
|
bskel scan [--feature <id>] [--terms a,b,c] [--json] [--accept-low-confidence] [--db [--database-url-env <NAME>] [--schema public]]
|
|
66
80
|
bskel scan disposition --feature <id> --mode reuse|extend|replace|parallel [--module <name>] [--note "..."] [--breaking-approved]
|
|
67
81
|
bskel scan explain <module> --feature <id> [--json]
|
|
82
|
+
bskel scan cross-feature-check --feature <id> [--db [--database-url-env <NAME>] [--schema public]] [--json]
|
|
83
|
+
bskel scan cross-feature-waive --feature <id> --signal resource_type|table|operation_id|db_foreign_key --identifier <name> --other-feature <id> --reason "..."
|
|
68
84
|
bskel feature init --slug <name>
|
|
69
85
|
bskel feature list [--all] [--json]
|
|
70
86
|
bskel feature show <id> [--json]
|
|
@@ -77,14 +93,22 @@ function usage() {
|
|
|
77
93
|
bskel contract validate --feature <id> --file <envelope.json>
|
|
78
94
|
bskel contract tool-schema --feature <id> --operation <operationId>
|
|
79
95
|
bskel contract waive --feature <id> --code <CODE> (--subject "VERB /path"|--all) --reason "..." [--expires <Nd>]
|
|
96
|
+
bskel dependency declare --feature <id> --resource <Type> --field <name> --source-feature <id> --source-resource <Type> --source-field <name> --reason "..." [--memo "..."]
|
|
97
|
+
bskel dependency remove --feature <id> --resource <Type> --field <name> --source-feature <id> --source-resource <Type> --source-field <name> --reason "..."
|
|
98
|
+
bskel dependency list --feature <id> [--json]
|
|
80
99
|
bskel stack apply --choice <id> [--apply] [--port N] [--json]
|
|
81
100
|
bskel catalog lint [<choice>] [--json]
|
|
82
101
|
bskel handles plan --feature <id> [--module <name>] [--resource type1,type2] [--diff] [--ast]
|
|
83
102
|
bskel handles emit --feature <id> [--module <name>] [--resource type1,type2] [--force --reason "..."] [--check] [--diff] [--enforce-registry on|off --reason "..."]
|
|
84
103
|
bskel handles patch approve --feature <id> [--module <name>] --resource <Type> --field <name> --strategy patch-wrapper|null-means-unchanged --reason "..." [--json]
|
|
85
104
|
bskel handles audit --feature <id> --database-url-env <NAME> [--resource type1,type2] [--json]
|
|
105
|
+
bskel patch propose --feature <id> [--kind config-apply|ddl-apply] --choice <stackChoiceId> --target <config_check target path> --database-url-env <NAME> --schema <name> --sql-file <path> [--json]
|
|
106
|
+
bskel patch approve --feature <id> --transaction <id> --reason "..." [--json]
|
|
107
|
+
bskel patch apply --feature <id> --transaction <id> [--confirm <id-or-dropped-table-name>] [--json]
|
|
108
|
+
bskel patch rollback --feature <id> --transaction <id> --reason "..." [--force] [--json]
|
|
109
|
+
bskel patch list --feature <id> [--json]
|
|
86
110
|
bskel observe emit --feature <id> [--module <name>] [--force --reason "..."] [--check] [--diff] [--json]
|
|
87
|
-
bskel observe import --feature <id> --receipts <path> [--json]
|
|
111
|
+
bskel observe import --feature <id> --receipts <path> [--fail-on-violation] [--json]
|
|
88
112
|
bskel verify --feature <id> [--build [--allow-skip-build]] [--json]
|
|
89
113
|
bskel status [--feature <id>] [--json]
|
|
90
114
|
bskel next [--feature <id>] [--json]
|
|
@@ -93,8 +117,11 @@ function usage() {
|
|
|
93
117
|
bskel gate revoke <name> --reason "..." [--feature <id>]
|
|
94
118
|
bskel gate history <name> [--feature <id>] [--json]
|
|
95
119
|
bskel gate show [<name>] [--feature <id>]
|
|
96
|
-
bskel gate export --feature <id> [--out <path>] [--json]
|
|
120
|
+
bskel gate export --feature <id> [--out <path>] [--sign --key <privateKeyPath>] [--json]
|
|
121
|
+
bskel attest keygen --out <dir> [--force] [--json]
|
|
122
|
+
bskel attest verify --file <path> --pubkey <path> [--json]
|
|
97
123
|
bskel doctor [--workflow ${DOCTOR_WORKFLOWS.join('|')}] [--json]
|
|
124
|
+
bskel serve [--port N] [--host <addr>] [--database-url-env <NAME>] [--schema <name>] [--sign-key <path>] [--require-sign-key] [--json]
|
|
98
125
|
`);
|
|
99
126
|
}
|
|
100
127
|
|
|
@@ -384,6 +411,12 @@ function cmdGateExport(args) {
|
|
|
384
411
|
setContext('gate export', flags);
|
|
385
412
|
const root = requireRepoRoot();
|
|
386
413
|
requireValidFeatureId(flags.feature);
|
|
414
|
+
if (flags.sign && !flags.key) {
|
|
415
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', 'bskel gate export --sign requires --key <privateKeyPath>');
|
|
416
|
+
}
|
|
417
|
+
if (flags.key && !flags.sign) {
|
|
418
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', '--key only has an effect together with --sign');
|
|
419
|
+
}
|
|
387
420
|
|
|
388
421
|
const gates = {};
|
|
389
422
|
for (const name of GATE_NAMES) {
|
|
@@ -398,14 +431,45 @@ function cmdGateExport(args) {
|
|
|
398
431
|
git: { branch: currentBranch(root), head_sha: headSha(root), dirty: isDirty(root) },
|
|
399
432
|
gates,
|
|
400
433
|
};
|
|
401
|
-
|
|
434
|
+
// D-gate-attestation-signing: validated unconditionally, signed or not -- a document that can
|
|
435
|
+
// be exported unsigned should be exactly as trustworthy in shape as one that gets signed later.
|
|
436
|
+
{
|
|
437
|
+
const { ok, errors } = validateAgainstSchema('gate-export.schema.json', report);
|
|
438
|
+
if (!ok) {
|
|
439
|
+
fail(EXIT_CODES.NOT_PASSED, 'INVALID_ARTIFACT', `internal error: the computed gate-export report failed its own schema -- ${formatSchemaErrors(errors).join('; ')}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
let payload = report;
|
|
444
|
+
if (flags.sign) {
|
|
445
|
+
let privateKeyPem;
|
|
446
|
+
try {
|
|
447
|
+
privateKeyPem = fs.readFileSync(path.resolve(process.cwd(), flags.key), 'utf8');
|
|
448
|
+
} catch (err) {
|
|
449
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', `could not read --key "${flags.key}": ${err.message}`);
|
|
450
|
+
}
|
|
451
|
+
let signatureValue;
|
|
452
|
+
try {
|
|
453
|
+
signatureValue = signPayload(report, privateKeyPem);
|
|
454
|
+
} catch (err) {
|
|
455
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', `--key "${flags.key}" is not a usable Ed25519 private key: ${err.message}`);
|
|
456
|
+
}
|
|
457
|
+
const attestation = { schema: 'sbf.gate-attestation/1', report, signature: { algorithm: 'ed25519', value: signatureValue } };
|
|
458
|
+
const { ok, errors } = validateAgainstSchema('gate-attestation.schema.json', attestation);
|
|
459
|
+
if (!ok) {
|
|
460
|
+
fail(EXIT_CODES.NOT_PASSED, 'INVALID_ARTIFACT', `internal error: the computed gate attestation failed its own schema -- ${formatSchemaErrors(errors).join('; ')}`);
|
|
461
|
+
}
|
|
462
|
+
payload = attestation;
|
|
463
|
+
}
|
|
464
|
+
const rendered = `${JSON.stringify(payload, null, 2)}\n`;
|
|
402
465
|
|
|
403
466
|
if (flags.out) {
|
|
404
467
|
const outPath = path.resolve(process.cwd(), flags.out);
|
|
405
468
|
writeFileAtomic(outPath, rendered);
|
|
406
469
|
if (!flags.quiet) {
|
|
407
470
|
const passCount = GATE_NAMES.filter((n) => gates[n].current?.status === 'pass').length;
|
|
408
|
-
|
|
471
|
+
const signedNote = flags.sign ? ' (signed)' : '';
|
|
472
|
+
console.log(`wrote ${flags.out}${signedNote} -- ${passCount}/${GATE_NAMES.length} gate(s) currently passing, ${report.git.branch}@${report.git.head_sha?.slice(0, 12) ?? '(unknown)'}${report.git.dirty ? ' (dirty)' : ''}`);
|
|
409
473
|
}
|
|
410
474
|
} else {
|
|
411
475
|
console.log(rendered);
|
|
@@ -413,6 +477,78 @@ function cmdGateExport(args) {
|
|
|
413
477
|
process.exit(0);
|
|
414
478
|
}
|
|
415
479
|
|
|
480
|
+
// D-gate-attestation-signing: --out is always required, never a default/home-directory location --
|
|
481
|
+
// this codebase has zero existing home-directory persistence convention anywhere, and inventing
|
|
482
|
+
// one is explicitly out of scope for this slice (a real fork the user weighed and decided, see
|
|
483
|
+
// DECISIONS.md). Repo-independent -- does not require a git repo at all, matching `attest verify`'s
|
|
484
|
+
// own posture below (both operate purely on files the caller names).
|
|
485
|
+
function cmdAttestKeygen(args) {
|
|
486
|
+
const flags = parseCommand('attest keygen', args);
|
|
487
|
+
if (flags.help) { console.log(renderCommandHelp('attest keygen')); process.exit(0); }
|
|
488
|
+
setContext('attest keygen', flags);
|
|
489
|
+
const outDir = path.resolve(process.cwd(), flags.out);
|
|
490
|
+
const privatePath = path.join(outDir, 'attest-private.pem');
|
|
491
|
+
const publicPath = path.join(outDir, 'attest-public.pem');
|
|
492
|
+
if (!flags.force) {
|
|
493
|
+
const existing = [privatePath, publicPath].filter((p) => fs.existsSync(p));
|
|
494
|
+
if (existing.length > 0) {
|
|
495
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', `refusing to overwrite existing key file(s) without --force: ${existing.join(', ')} -- regenerating would orphan any attestation already signed with the old key`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const { publicKeyPem, privateKeyPem } = generateKeypair();
|
|
499
|
+
// Restrictive mode (owner read/write only) from the very first write -- see the
|
|
500
|
+
// `D-gate-attestation-signing` entry's own note in `lib/fsutil.mjs` on why this is a
|
|
501
|
+
// `writeFileAtomic` parameter, not a chmod() called after the fact.
|
|
502
|
+
writeFileAtomic(privatePath, privateKeyPem, 0o600);
|
|
503
|
+
writeFileAtomic(publicPath, publicKeyPem);
|
|
504
|
+
|
|
505
|
+
if (flags.json) {
|
|
506
|
+
console.log(JSON.stringify({ private_key: privatePath, public_key: publicPath }, null, 2));
|
|
507
|
+
} else if (!flags.quiet) {
|
|
508
|
+
console.log(`wrote ${privatePath} (0600) and ${publicPath}`);
|
|
509
|
+
}
|
|
510
|
+
process.exit(0);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Deliberately repo-independent (no requireRepoRoot()) -- verifying a previously-exported
|
|
514
|
+
// attestation has nothing to do with the current directory's own git state; the whole point is to
|
|
515
|
+
// check a document someone else produced, possibly on a different machine, offline. Exit code is
|
|
516
|
+
// driven ONLY by signature validity -- whether the gates INSIDE the report passed is a separate,
|
|
517
|
+
// printed question (see DECISIONS.md for why conflating the two would be actively misleading).
|
|
518
|
+
function cmdAttestVerify(args) {
|
|
519
|
+
const flags = parseCommand('attest verify', args);
|
|
520
|
+
if (flags.help) { console.log(renderCommandHelp('attest verify')); process.exit(0); }
|
|
521
|
+
setContext('attest verify', flags);
|
|
522
|
+
|
|
523
|
+
let attestation;
|
|
524
|
+
try {
|
|
525
|
+
attestation = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), flags.file), 'utf8'));
|
|
526
|
+
} catch (err) {
|
|
527
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', `could not read/parse --file "${flags.file}": ${err.message}`);
|
|
528
|
+
}
|
|
529
|
+
const { ok: schemaOk, errors: schemaErrors } = validateAgainstSchema('gate-attestation.schema.json', attestation);
|
|
530
|
+
if (!schemaOk) {
|
|
531
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', `"${flags.file}" is not a valid gate attestation: ${formatSchemaErrors(schemaErrors).join('; ')}`);
|
|
532
|
+
}
|
|
533
|
+
let publicKeyPem;
|
|
534
|
+
try {
|
|
535
|
+
publicKeyPem = fs.readFileSync(path.resolve(process.cwd(), flags.pubkey), 'utf8');
|
|
536
|
+
} catch (err) {
|
|
537
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', `could not read --pubkey "${flags.pubkey}": ${err.message}`);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const valid = verifyPayload(attestation.report, attestation.signature.value, publicKeyPem);
|
|
541
|
+
const passCount = GATE_NAMES.filter((n) => attestation.report.gates[n]?.current?.status === 'pass').length;
|
|
542
|
+
|
|
543
|
+
if (flags.json) {
|
|
544
|
+
console.log(JSON.stringify({ valid, report_summary: { feature_id: attestation.report.feature_id, generated_at: attestation.report.generated_at, gates_passing: `${passCount}/${GATE_NAMES.length}` } }, null, 2));
|
|
545
|
+
} else if (!flags.quiet) {
|
|
546
|
+
console.log(valid ? 'VALID: this attestation was genuinely signed by the holder of the matching private key' : 'INVALID: signature does not match this report + public key');
|
|
547
|
+
console.log(`report says: feature ${attestation.report.feature_id}, ${passCount}/${GATE_NAMES.length} gate(s) passing, generated ${attestation.report.generated_at}`);
|
|
548
|
+
}
|
|
549
|
+
process.exit(valid ? EXIT_CODES.OK : EXIT_CODES.CHECK_FAILED);
|
|
550
|
+
}
|
|
551
|
+
|
|
416
552
|
// Structural enforcement of "preflight blocks everything below it" (see the workflow table in
|
|
417
553
|
// SKILL.md) for every feature-scoped command -- not just documented as a step order, checked.
|
|
418
554
|
// Ad-hoc `bskel scan` (no --feature) is exempt: it's an explicit side-channel quick-look
|
|
@@ -625,6 +761,146 @@ function cmdScanExplain(args) {
|
|
|
625
761
|
process.exit(0);
|
|
626
762
|
}
|
|
627
763
|
|
|
764
|
+
// D-cross-feature-collision: mirrors cmdContractEmit's own "always write the artifact, gate
|
|
765
|
+
// blocks only if unresolved issues remain" shape exactly, for a different data source (NAME-
|
|
766
|
+
// identity collisions against every OTHER feature, not this feature's own contract completeness).
|
|
767
|
+
//
|
|
768
|
+
// D-cross-feature-fk-inference: async now -- reuses the EXACT same resolveDbSchemaOrExit() helper
|
|
769
|
+
// cmdScan() already calls for `--db [--database-url-env <NAME>] [--schema public]`, so this command
|
|
770
|
+
// gains those same flags with zero new live-DB code path. If `--db` was never given (the existing,
|
|
771
|
+
// unmodified call shape every current caller -- 5 CI smoke scripts, README's Quickstart -- already
|
|
772
|
+
// uses), findCollisions() falls back to a persisted snapshot or reports fk_check:'unavailable';
|
|
773
|
+
// the first 3 signals' findings and blocking behavior are byte-identical to before this item.
|
|
774
|
+
async function cmdScanCrossFeatureCheck(args) {
|
|
775
|
+
const flags = parseCommand('scan cross-feature-check', args);
|
|
776
|
+
if (flags.help) { console.log(renderCommandHelp('scan cross-feature-check')); process.exit(0); }
|
|
777
|
+
setContext('scan cross-feature-check', flags);
|
|
778
|
+
const root = requireRepoRoot();
|
|
779
|
+
requireValidFeatureId(flags.feature);
|
|
780
|
+
|
|
781
|
+
const liveDbSchema = await resolveDbSchemaOrExit(root, flags);
|
|
782
|
+
const { findings, fk_check, unknowns } = findCollisions(root, flags.feature, { liveDbSchema });
|
|
783
|
+
const report = {
|
|
784
|
+
schema: 'sbf.cross-feature-report/1',
|
|
785
|
+
feature_id: flags.feature,
|
|
786
|
+
generated_at: new Date().toISOString(),
|
|
787
|
+
findings,
|
|
788
|
+
fk_check,
|
|
789
|
+
unknowns,
|
|
790
|
+
};
|
|
791
|
+
const { ok, errors } = validateAgainstSchema('cross-feature-report.schema.json', report);
|
|
792
|
+
if (!ok) {
|
|
793
|
+
fail(EXIT_CODES.NOT_PASSED, 'INVALID_ARTIFACT', `internal error: the computed cross-feature report failed its own schema -- ${formatSchemaErrors(errors).join('; ')}`);
|
|
794
|
+
}
|
|
795
|
+
writeFileAtomic(crossFeatureReportPath(root, flags.feature), `${JSON.stringify(report, null, 2)}\n`);
|
|
796
|
+
|
|
797
|
+
const resolution = loadCrossFeatureResolution(root, flags.feature);
|
|
798
|
+
const evaluation = evaluateCrossFeatureFindings(findings, resolution);
|
|
799
|
+
const evidence = {
|
|
800
|
+
finding_count: findings.length,
|
|
801
|
+
high_confidence_count: findings.filter((f) => f.confidence === 'high').length,
|
|
802
|
+
waived_count: evaluation.waived.length,
|
|
803
|
+
stale_waivers: evaluation.staleWaivers.length,
|
|
804
|
+
};
|
|
805
|
+
const gateState = evaluation.blocking
|
|
806
|
+
? awaitNamedGateDisposition(root, 'cross_feature', flags.feature, { ...evidence, unwaived: evaluation.unwaived })
|
|
807
|
+
: passNamedGate(root, 'cross_feature', flags.feature, evidence);
|
|
808
|
+
|
|
809
|
+
if (flags.json) {
|
|
810
|
+
console.log(JSON.stringify({ report, gate: gateState.gates.cross_feature }, null, 2));
|
|
811
|
+
} else {
|
|
812
|
+
if (!flags.quiet) {
|
|
813
|
+
const otherCount = new Set(findings.map((f) => f.other_feature)).size;
|
|
814
|
+
console.log(`${findings.length} finding(s) (${evidence.high_confidence_count} high-confidence) across ${otherCount} other feature(s)`);
|
|
815
|
+
for (const f of findings) console.log(` [${f.confidence}] ${f.signal}: "${f.identifier}" also declared by ${f.other_feature}`);
|
|
816
|
+
// D-cross-feature-fk-inference (staleness/freshness token): the actual point of the field
|
|
817
|
+
// -- a human SEEING how stale a persisted/migrations-mode correlation is, not just the
|
|
818
|
+
// JSON carrying it silently.
|
|
819
|
+
const fkCheckDetails = [];
|
|
820
|
+
if (fk_check.source_feature) fkCheckDetails.push(`from ${fk_check.source_feature}'s own persisted snapshot`);
|
|
821
|
+
if (fk_check.generated_at) fkCheckDetails.push(`captured ${fk_check.generated_at}`);
|
|
822
|
+
console.log(`fk_check: ${fk_check.mode}${fkCheckDetails.length ? ` (${fkCheckDetails.join(', ')})` : ''}`);
|
|
823
|
+
for (const u of unknowns) console.log(` note: ${u}`);
|
|
824
|
+
console.log(`gate: cross_feature -> ${gateState.gates.cross_feature.status}`);
|
|
825
|
+
}
|
|
826
|
+
if (evaluation.staleWaivers.length > 0) {
|
|
827
|
+
console.error(`\nnote: ${evaluation.staleWaivers.length} recorded waiver(s) no longer match any current finding (kept as-is, not auto-removed):`);
|
|
828
|
+
for (const w of evaluation.staleWaivers) console.error(` ${w.signal} "${w.identifier}" (${w.other_feature})`);
|
|
829
|
+
}
|
|
830
|
+
if (evaluation.blocking) {
|
|
831
|
+
console.error(`\nblocked: ${evaluation.unwaived.length} unresolved high-confidence collision(s):`);
|
|
832
|
+
for (const f of evaluation.unwaived) {
|
|
833
|
+
console.error(` bskel scan cross-feature-waive --feature ${flags.feature} --signal ${f.signal} --identifier "${f.identifier}" --other-feature ${f.other_feature} --reason "..."`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
process.exit(evaluation.blocking ? EXIT.AWAITING_DISPOSITION : EXIT.PASS);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const CROSS_FEATURE_SIGNALS = ['resource_type', 'table', 'operation_id', 'db_foreign_key'];
|
|
841
|
+
|
|
842
|
+
// Validates against the PERSISTED report from the last `cross-feature-check` run, never a live
|
|
843
|
+
// re-computation -- same precedent `contract waive` already establishes against `loadContract`
|
|
844
|
+
// (contracts/completeness.mjs). If reality moved since that check, the gate's own staleness token
|
|
845
|
+
// (which covers every OTHER feature named in the report) is what surfaces that, not a silent
|
|
846
|
+
// re-check inside this command.
|
|
847
|
+
function cmdScanCrossFeatureWaive(args) {
|
|
848
|
+
const flags = parseCommand('scan cross-feature-waive', args);
|
|
849
|
+
if (flags.help) { console.log(renderCommandHelp('scan cross-feature-waive')); process.exit(0); }
|
|
850
|
+
setContext('scan cross-feature-waive', flags);
|
|
851
|
+
const root = requireRepoRoot();
|
|
852
|
+
requireValidFeatureId(flags.feature);
|
|
853
|
+
requireValidFeatureId(flags['other-feature']);
|
|
854
|
+
if (!flags.reason || !flags.reason.trim()) {
|
|
855
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', 'bskel scan cross-feature-waive requires --reason "..." -- every waiver must be auditable');
|
|
856
|
+
}
|
|
857
|
+
if (!CROSS_FEATURE_SIGNALS.includes(flags.signal)) {
|
|
858
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', `--signal must be one of: ${CROSS_FEATURE_SIGNALS.join(', ')}`);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const report = loadCrossFeatureReport(root, flags.feature);
|
|
862
|
+
if (!report) {
|
|
863
|
+
fail(EXIT_CODES.NOT_PASSED, 'MISSING_ARTIFACT', `no cross-feature-report.json for feature "${flags.feature}" -- run \`bskel scan cross-feature-check --feature ${flags.feature}\` first`);
|
|
864
|
+
}
|
|
865
|
+
const match = report.findings.find((f) => f.signal === flags.signal && f.identifier === flags.identifier && f.other_feature === flags['other-feature']);
|
|
866
|
+
if (!match) {
|
|
867
|
+
const known = report.findings.map((f) => `${f.signal} "${f.identifier}" (${f.other_feature})`);
|
|
868
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', `no current finding matches --signal ${flags.signal} --identifier "${flags.identifier}" --other-feature ${flags['other-feature']} -- current findings: ${known.join('; ') || '(none)'}`);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const updated = withLockSync(root, 'state', () => {
|
|
872
|
+
const resolution = loadCrossFeatureResolution(root, flags.feature);
|
|
873
|
+
const key = waiverKey({ signal: flags.signal, identifier: flags.identifier, other_feature: flags['other-feature'] });
|
|
874
|
+
const entry = { signal: flags.signal, identifier: flags.identifier, other_feature: flags['other-feature'], reason: flags.reason, at: new Date().toISOString() };
|
|
875
|
+
const next = {
|
|
876
|
+
schema: 'sbf.cross-feature-resolution/1',
|
|
877
|
+
feature_id: flags.feature,
|
|
878
|
+
waivers: [...resolution.waivers.filter((w) => waiverKey(w) !== key), entry],
|
|
879
|
+
};
|
|
880
|
+
saveCrossFeatureResolution(root, flags.feature, next);
|
|
881
|
+
return next;
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
const evaluation = evaluateCrossFeatureFindings(report.findings, updated);
|
|
885
|
+
const evidence = {
|
|
886
|
+
finding_count: report.findings.length,
|
|
887
|
+
high_confidence_count: report.findings.filter((f) => f.confidence === 'high').length,
|
|
888
|
+
waived_count: evaluation.waived.length,
|
|
889
|
+
stale_waivers: evaluation.staleWaivers.length,
|
|
890
|
+
};
|
|
891
|
+
const gateState = evaluation.blocking
|
|
892
|
+
? awaitNamedGateDisposition(root, 'cross_feature', flags.feature, { ...evidence, unwaived: evaluation.unwaived })
|
|
893
|
+
: passNamedGate(root, 'cross_feature', flags.feature, evidence);
|
|
894
|
+
|
|
895
|
+
if (flags.json) {
|
|
896
|
+
console.log(JSON.stringify({ waived: true, gate: gateState.gates.cross_feature }, null, 2));
|
|
897
|
+
} else if (!flags.quiet) {
|
|
898
|
+
console.log(`waived: ${flags.signal} "${flags.identifier}" (${flags['other-feature']})`);
|
|
899
|
+
console.log(`gate: cross_feature -> ${gateState.gates.cross_feature.status}`);
|
|
900
|
+
}
|
|
901
|
+
process.exit(evaluation.blocking ? EXIT.AWAITING_DISPOSITION : EXIT.PASS);
|
|
902
|
+
}
|
|
903
|
+
|
|
628
904
|
// D6 (D-feature-lifecycle): the whole read-specs/->compute-NNN->write-feature.json->
|
|
629
905
|
// load-modify-save-feature-index.json sequence runs under one exclusive lock -- confirmed live
|
|
630
906
|
// during this item's own grounding, the same lost-update shape S5 already fixed for setGate():
|
|
@@ -1055,6 +1331,7 @@ function cmdContractEmit(args) {
|
|
|
1055
1331
|
console.error(`\nnote: ${evaluation.staleWaivers.length} recorded waiver(s) no longer match any current warning (kept as-is, not auto-removed):`);
|
|
1056
1332
|
for (const w of evaluation.staleWaivers) console.error(` ${w.code} (${w.subject ?? '*'})`);
|
|
1057
1333
|
}
|
|
1334
|
+
for (const n of describeDownstreamImpact(root, flags.feature)) console.error(`\nnote: ${n}`);
|
|
1058
1335
|
if (evaluation.blocking) {
|
|
1059
1336
|
if (evaluation.status === 'blocked') {
|
|
1060
1337
|
console.error(`\nblocked: this contract has zero operations and cannot be waived -- fix --module/--terms, or run \`bskel gate force contract --feature ${flags.feature} --reason "..."\` if this module genuinely has no HTTP surface (yet).`);
|
|
@@ -1363,6 +1640,123 @@ function cmdContractWaive(args) {
|
|
|
1363
1640
|
process.exit(evaluation.blocking ? EXIT.AWAITING_DISPOSITION : EXIT.PASS);
|
|
1364
1641
|
}
|
|
1365
1642
|
|
|
1643
|
+
// D-dependency-propagation-notice: called from cmdContractEmit/cmdHandlesEmit to warn a SOURCE
|
|
1644
|
+
// feature, at the moment its own generated artifacts are refreshed, that other features declared a
|
|
1645
|
+
// dependency on one of its fields. Only surfaces a note when the dependent's OWN `dependencies` gate
|
|
1646
|
+
// is actually stale AND that staleness is attributable to THIS featureId specifically (a
|
|
1647
|
+
// `source_field_file:<featureId>:` key in its changed_inputs) -- a dependent that's stale for some
|
|
1648
|
+
// OTHER, unrelated reason must not be misattributed to this feature's own change. When
|
|
1649
|
+
// changed_inputs can't explain the staleness (NO_RECORDED_INPUTS/RECORDED_INPUTS_MISMATCH), the note
|
|
1650
|
+
// is skipped rather than guessed -- this is a best-effort nudge, never the source of truth for
|
|
1651
|
+
// whether something is actually stale (bskel verify/status on the dependent feature itself remains
|
|
1652
|
+
// that source of truth).
|
|
1653
|
+
function describeDownstreamImpact(root, featureId) {
|
|
1654
|
+
const byDependent = new Map();
|
|
1655
|
+
for (const { dependentFeature, dep } of listDownstreamDependents(root, featureId)) {
|
|
1656
|
+
if (!byDependent.has(dependentFeature)) byDependent.set(dependentFeature, []);
|
|
1657
|
+
byDependent.get(dependentFeature).push(dep);
|
|
1658
|
+
}
|
|
1659
|
+
const notes = [];
|
|
1660
|
+
const prefix = `source_field_file:${featureId}:`;
|
|
1661
|
+
for (const [dependentFeature, deps] of byDependent) {
|
|
1662
|
+
const gate = requireNamedGate(root, 'dependencies', dependentFeature);
|
|
1663
|
+
if (gate.status !== 'stale') continue;
|
|
1664
|
+
if (!(gate.changed_inputs ?? []).some((k) => k.startsWith(prefix))) continue;
|
|
1665
|
+
const list = deps.map((d) => `${d.target.resourceType}.${d.target.fieldName} <- ${d.source.resourceType}.${d.source.fieldName}`).join('; ');
|
|
1666
|
+
notes.push(
|
|
1667
|
+
`downstream impact: feature "${dependentFeature}" depends on this feature's field(s) (${list}), and that dependency just went stale -- ` +
|
|
1668
|
+
`review with \`bskel dependency list --feature ${dependentFeature} --json\`, then re-run \`bskel dependency declare ...\` once the change is accounted for.`,
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
1671
|
+
return notes;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
// D-http-serving-layer: cmdDependencyDeclare/Remove/List are now thin CLI wrappers over
|
|
1675
|
+
// lib/field-dependencies.mjs's declareDependency/removeDependency/buildDependencyListReport --
|
|
1676
|
+
// lib/http-server.mjs's POST/DELETE/GET handlers call the SAME functions, so the CLI and HTTP
|
|
1677
|
+
// surfaces can never diverge on what these operations actually do. A thrown DependencyOperationError
|
|
1678
|
+
// carries the exact (exitCode, reasonCode) this CLI path always used -- fail() is called with those
|
|
1679
|
+
// verbatim, so this refactor is behavior-preserving (verified: test/dependency-cli.test.mjs, written
|
|
1680
|
+
// before this refactor existed, passes unchanged).
|
|
1681
|
+
function cmdDependencyDeclare(args) {
|
|
1682
|
+
const flags = parseCommand('dependency declare', args);
|
|
1683
|
+
if (flags.help) { console.log(renderCommandHelp('dependency declare')); process.exit(0); }
|
|
1684
|
+
setContext('dependency declare', flags);
|
|
1685
|
+
const root = requireRepoRoot();
|
|
1686
|
+
let result;
|
|
1687
|
+
try {
|
|
1688
|
+
result = declareDependency(root, {
|
|
1689
|
+
feature: flags.feature, resource: flags.resource, field: flags.field,
|
|
1690
|
+
sourceFeature: flags['source-feature'], sourceResource: flags['source-resource'], sourceField: flags['source-field'],
|
|
1691
|
+
reason: flags.reason, memo: flags.memo,
|
|
1692
|
+
});
|
|
1693
|
+
} catch (err) {
|
|
1694
|
+
if (err instanceof DependencyOperationError) fail(err.exitCode, err.reasonCode, err.message);
|
|
1695
|
+
throw err;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
if (flags.json) {
|
|
1699
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1700
|
+
} else if (!flags.quiet) {
|
|
1701
|
+
console.log(`declared: ${flags.resource}.${flags.field} <- ${flags['source-feature']}/${flags['source-resource']}.${flags['source-field']}`);
|
|
1702
|
+
console.log(`gate: dependencies -> ${result.gate.status}`);
|
|
1703
|
+
}
|
|
1704
|
+
process.exit(EXIT.PASS);
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
function cmdDependencyRemove(args) {
|
|
1708
|
+
const flags = parseCommand('dependency remove', args);
|
|
1709
|
+
if (flags.help) { console.log(renderCommandHelp('dependency remove')); process.exit(0); }
|
|
1710
|
+
setContext('dependency remove', flags);
|
|
1711
|
+
const root = requireRepoRoot();
|
|
1712
|
+
let result;
|
|
1713
|
+
try {
|
|
1714
|
+
result = removeDependency(root, {
|
|
1715
|
+
feature: flags.feature, resource: flags.resource, field: flags.field,
|
|
1716
|
+
sourceFeature: flags['source-feature'], sourceResource: flags['source-resource'], sourceField: flags['source-field'],
|
|
1717
|
+
reason: flags.reason,
|
|
1718
|
+
});
|
|
1719
|
+
} catch (err) {
|
|
1720
|
+
if (err instanceof DependencyOperationError) fail(err.exitCode, err.reasonCode, err.message);
|
|
1721
|
+
throw err;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
if (flags.json) {
|
|
1725
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1726
|
+
} else if (!flags.quiet) {
|
|
1727
|
+
console.log(`removed: ${flags.resource}.${flags.field} <- ${flags['source-feature']}/${flags['source-resource']}.${flags['source-field']}`);
|
|
1728
|
+
console.log(`gate: dependencies -> ${result.gate.status}`);
|
|
1729
|
+
}
|
|
1730
|
+
process.exit(EXIT.PASS);
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
function cmdDependencyList(args) {
|
|
1734
|
+
const flags = parseCommand('dependency list', args);
|
|
1735
|
+
if (flags.help) { console.log(renderCommandHelp('dependency list')); process.exit(0); }
|
|
1736
|
+
setContext('dependency list', flags);
|
|
1737
|
+
const root = requireRepoRoot();
|
|
1738
|
+
let report;
|
|
1739
|
+
try {
|
|
1740
|
+
report = buildDependencyListReport(root, flags.feature);
|
|
1741
|
+
} catch (err) {
|
|
1742
|
+
if (err instanceof DependencyOperationError) fail(err.exitCode, err.reasonCode, err.message);
|
|
1743
|
+
throw err;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
if (flags.json) {
|
|
1747
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1748
|
+
} else {
|
|
1749
|
+
console.log(`dependencies -- feature ${flags.feature} (gate: ${report.gate.status})`);
|
|
1750
|
+
for (const r of report.dependencies) {
|
|
1751
|
+
const tNote = r.target_resolved ? 'ok' : `UNRESOLVED:${r.target_unresolved_reason}`;
|
|
1752
|
+
const sNote = r.source_resolved ? 'ok' : `UNRESOLVED:${r.source_unresolved_reason}`;
|
|
1753
|
+
console.log(` ${r.target.resourceType}.${r.target.fieldName} [${tNote}] <- ${r.source.feature}/${r.source.resourceType}.${r.source.fieldName} [${sNote}]`);
|
|
1754
|
+
}
|
|
1755
|
+
if (report.dependencies.length === 0) console.log(' (none declared)');
|
|
1756
|
+
}
|
|
1757
|
+
process.exit(0);
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1366
1760
|
// D-contract-history: a derived VIEW over the contract file's own git history in whatever repo
|
|
1367
1761
|
// bskel is invoked in -- reads, never writes. Deliberately does NOT try to correlate a commit to
|
|
1368
1762
|
// a specific `.sbf/<feature>.history.jsonl` gate-pass event: that file is per-machine, gitignored,
|
|
@@ -1909,6 +2303,25 @@ function cmdHandlesEmit(args) {
|
|
|
1909
2303
|
});
|
|
1910
2304
|
}
|
|
1911
2305
|
|
|
2306
|
+
// D-cross-feature-collision: making "mandatory disposition" actually mandatory, not just
|
|
2307
|
+
// available -- `handles emit` is specifically the step that generates the runtime resolver
|
|
2308
|
+
// code every codegen provider's own resourceType-keyed dispatch (Java/Python/TS, all three)
|
|
2309
|
+
// already implicitly assumes is repo-unique. `cross_feature`'s own gate `verifyPolicy` stays
|
|
2310
|
+
// REQUIRED_WHEN_PRESENT (so `bskel verify` doesn't retroactively fail a contract-only feature
|
|
2311
|
+
// that never touches handles at all) -- this hard-requires it ONLY here, the one command where
|
|
2312
|
+
// the real risk actually lives, mirroring the `contract` gate check immediately above exactly.
|
|
2313
|
+
// Deliberately NOT added to `cmdHandlesPlan` -- that command never writes (dryRun always),
|
|
2314
|
+
// same reasoning it already gives for skipping the `contract` gate entirely.
|
|
2315
|
+
const crossFeatureResult = requireNamedGate(root, 'cross_feature', flags.feature);
|
|
2316
|
+
if (crossFeatureResult.code !== EXIT.PASS) {
|
|
2317
|
+
const cfHint = crossFeatureResult.status === 'awaiting_disposition'
|
|
2318
|
+
? `resolve it first -- \`bskel scan cross-feature-waive --feature ${flags.feature} --signal resource_type|table|operation_id|db_foreign_key --identifier <name> --other-feature <id> --reason "..."\`, or \`bskel gate force cross_feature --feature ${flags.feature} --reason "..."\` if intentional.`
|
|
2319
|
+
: `run \`bskel scan cross-feature-check --feature ${flags.feature}\` first.`;
|
|
2320
|
+
fail(crossFeatureResult.code, gateReasonForCode(crossFeatureResult.code), `blocked: \`cross_feature\` gate for ${flags.feature} is ${crossFeatureResult.status} -- ${cfHint}`, {
|
|
2321
|
+
next_actions: [{ command: `bskel scan cross-feature-check --feature ${flags.feature}`, reason: 'the cross_feature gate has not passed yet', mutating: true }],
|
|
2322
|
+
});
|
|
2323
|
+
}
|
|
2324
|
+
|
|
1912
2325
|
const scanReport = loadScanReportOrExit(root, flags.feature);
|
|
1913
2326
|
const scanReportPath = specPath(root, flags.feature, 'brownfield-scan.json');
|
|
1914
2327
|
requireCapabilitiesOrExit(scanReport, 'handles emit', { featureId: flags.feature, scanReportPath });
|
|
@@ -1990,6 +2403,11 @@ function cmdHandlesEmit(args) {
|
|
|
1990
2403
|
// D4: dryRun never marks the gate passed -- nothing real happened this run.
|
|
1991
2404
|
const gateState = dryRun ? null : passNamedGate(root, 'handles', flags.feature, { resolverStubs });
|
|
1992
2405
|
|
|
2406
|
+
// D-dependency-propagation-notice: appended here (not inside any provider's own emit.mjs) so it
|
|
2407
|
+
// applies uniformly regardless of which provider ran -- inherits the same --json/text-mode
|
|
2408
|
+
// visibility every provider-authored postEmitNote already has, no special-casing needed.
|
|
2409
|
+
postEmitNotes.push(...describeDownstreamImpact(root, flags.feature));
|
|
2410
|
+
|
|
1993
2411
|
if (flags.json) {
|
|
1994
2412
|
console.log(JSON.stringify({ written, resolverStubs, conflicts, orphans, forced, notes: allNotes, actions, blocked: false, gate: gateState?.gates.handles ?? null, check: dryRun, postEmitNotes }, null, 2));
|
|
1995
2413
|
} else if (!flags.quiet) {
|
|
@@ -2079,6 +2497,189 @@ function cmdHandlesPatchApprove(args) {
|
|
|
2079
2497
|
process.exit(0);
|
|
2080
2498
|
}
|
|
2081
2499
|
|
|
2500
|
+
// D-patch-transactions: Slice 1 (config_check -> config_apply). All four commands only touch
|
|
2501
|
+
// specs/<featureId>/patch-transactions/ except `apply`/`rollback`, which write to the real target
|
|
2502
|
+
// file too -- matching D4's own "propose/approve are specs/-only, apply/rollback touch the repo"
|
|
2503
|
+
// distinction cmdHandlesEmit/cmdHandlesPlan already draw for --check vs a real emit.
|
|
2504
|
+
// D-ddl-apply: `--kind` picks which patch-transaction kind to propose (default 'config-apply',
|
|
2505
|
+
// fully backward compatible -- every existing call site/script that never passed --kind gets the
|
|
2506
|
+
// exact prior behavior). Kind-specific params are validated by hand here (not via COMMANDS'
|
|
2507
|
+
// declarative `required: true`, which is unconditional per-flag) -- matches this file's own
|
|
2508
|
+
// existing convention for kind-conditional requirements (e.g. --reason on approve/rollback).
|
|
2509
|
+
async function cmdPatchPropose(args) {
|
|
2510
|
+
const flags = parseCommand('patch propose', args);
|
|
2511
|
+
if (flags.help) { console.log(renderCommandHelp('patch propose')); process.exit(0); }
|
|
2512
|
+
setContext('patch propose', flags);
|
|
2513
|
+
const root = requireRepoRoot();
|
|
2514
|
+
requireValidFeatureId(flags.feature);
|
|
2515
|
+
const kind = flags.kind || 'config-apply';
|
|
2516
|
+
if (!PATCH_KIND_NAMES.includes(kind)) {
|
|
2517
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', `--kind must be one of: ${PATCH_KIND_NAMES.join(', ')}`);
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
let params;
|
|
2521
|
+
let source;
|
|
2522
|
+
if (kind === 'config-apply') {
|
|
2523
|
+
if (!flags.choice || !flags.target) {
|
|
2524
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', 'bskel patch propose --kind config-apply requires --choice <stackChoiceId> --target <config_check target path>');
|
|
2525
|
+
}
|
|
2526
|
+
params = { choice: flags.choice, target: flags.target };
|
|
2527
|
+
source = { choice: flags.choice };
|
|
2528
|
+
} else {
|
|
2529
|
+
if (!flags['database-url-env'] || !flags['sql-file']) {
|
|
2530
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', 'bskel patch propose --kind ddl-apply requires --database-url-env <NAME> --sql-file <path>');
|
|
2531
|
+
}
|
|
2532
|
+
let sqlText;
|
|
2533
|
+
try {
|
|
2534
|
+
sqlText = fs.readFileSync(flags['sql-file'], 'utf8');
|
|
2535
|
+
} catch (err) {
|
|
2536
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', `could not read --sql-file "${flags['sql-file']}": ${err.message}`);
|
|
2537
|
+
}
|
|
2538
|
+
params = { databaseUrlEnv: flags['database-url-env'], schema: flags.schema, sqlText };
|
|
2539
|
+
source = { database_url_env: flags['database-url-env'], schema: flags.schema };
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
let plan;
|
|
2543
|
+
try {
|
|
2544
|
+
plan = await getPatchKind(kind).planFresh(root, params);
|
|
2545
|
+
} catch (err) {
|
|
2546
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', err.message);
|
|
2547
|
+
}
|
|
2548
|
+
const txn = proposeTransaction(root, flags.feature, kind, plan, source);
|
|
2549
|
+
|
|
2550
|
+
if (flags.json) {
|
|
2551
|
+
console.log(JSON.stringify(txn, null, 2));
|
|
2552
|
+
} else if (!flags.quiet) {
|
|
2553
|
+
console.log(`proposed: ${txn.transaction_id}`);
|
|
2554
|
+
console.log(` ${describePatchTransaction(txn)}`);
|
|
2555
|
+
console.log(`next: bskel patch approve --feature ${flags.feature} --transaction ${txn.transaction_id} --reason "..."`);
|
|
2556
|
+
}
|
|
2557
|
+
process.exit(0);
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
// Kind-aware one-line summary, reused by propose/list's human-readable output -- config-apply's
|
|
2561
|
+
// target is a file+key_path, ddl-apply's is raw SQL text (truncated for a terminal line).
|
|
2562
|
+
function describePatchTransaction(txn) {
|
|
2563
|
+
if (txn.kind === 'config-apply') {
|
|
2564
|
+
return `${txn.target.file} @ ${txn.target.key_path.join('.')}: "${txn.current_value}" -> "${txn.proposed_value}"`;
|
|
2565
|
+
}
|
|
2566
|
+
const sql = txn.target.sql_text.replace(/\s+/g, ' ').trim();
|
|
2567
|
+
return `[${txn.kind}] ${txn.target.database_url_env}/${txn.target.schema}: ${sql.slice(0, 100)}${sql.length > 100 ? '...' : ''}`;
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
function requireTransactionOrExit(root, featureId, transactionId) {
|
|
2571
|
+
const txn = loadTransaction(root, featureId, transactionId);
|
|
2572
|
+
if (!txn) {
|
|
2573
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', `no patch transaction "${transactionId}" for feature "${featureId}"`);
|
|
2574
|
+
}
|
|
2575
|
+
return txn;
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
async function cmdPatchApprove(args) {
|
|
2579
|
+
const flags = parseCommand('patch approve', args);
|
|
2580
|
+
if (flags.help) { console.log(renderCommandHelp('patch approve')); process.exit(0); }
|
|
2581
|
+
setContext('patch approve', flags);
|
|
2582
|
+
const root = requireRepoRoot();
|
|
2583
|
+
requireValidFeatureId(flags.feature);
|
|
2584
|
+
if (!flags.reason || !flags.reason.trim()) {
|
|
2585
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', 'bskel patch approve requires --reason "..." -- every approval must be auditable');
|
|
2586
|
+
}
|
|
2587
|
+
const txn = requireTransactionOrExit(root, flags.feature, flags.transaction);
|
|
2588
|
+
|
|
2589
|
+
let freshPlan;
|
|
2590
|
+
try {
|
|
2591
|
+
freshPlan = await replanTransaction(root, txn);
|
|
2592
|
+
} catch (err) {
|
|
2593
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', err.message);
|
|
2594
|
+
}
|
|
2595
|
+
const updated = approveTransaction(root, flags.feature, flags.transaction, flags.reason, freshPlan);
|
|
2596
|
+
|
|
2597
|
+
console.log(flags.json ? JSON.stringify(updated, null, 2) : `approved: ${flags.transaction}`);
|
|
2598
|
+
process.exit(0);
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
// D-ddl-apply: --confirm is required (and must exactly equal --transaction) for any kind other
|
|
2602
|
+
// than 'config-apply' -- deliberately checked here, at the CLI boundary, before applyTransaction()
|
|
2603
|
+
// is ever called, as human-factors friction layered ON TOP of the engine's own load-bearing
|
|
2604
|
+
// preimage-hash TOCTOU check (this check does not replace it). Optional/ignored for config-apply,
|
|
2605
|
+
// zero behavior change for that kind.
|
|
2606
|
+
async function cmdPatchApply(args) {
|
|
2607
|
+
const flags = parseCommand('patch apply', args);
|
|
2608
|
+
if (flags.help) { console.log(renderCommandHelp('patch apply')); process.exit(0); }
|
|
2609
|
+
setContext('patch apply', flags);
|
|
2610
|
+
const root = requireRepoRoot();
|
|
2611
|
+
requirePreflightPassed(root);
|
|
2612
|
+
requireValidFeatureId(flags.feature);
|
|
2613
|
+
const txn = requireTransactionOrExit(root, flags.feature, flags.transaction);
|
|
2614
|
+
|
|
2615
|
+
const requiredConfirm = getPatchKind(txn.kind).requiredConfirmValue(txn);
|
|
2616
|
+
if (requiredConfirm !== null && flags.confirm !== requiredConfirm) {
|
|
2617
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', `bskel patch apply --kind ${txn.kind} requires --confirm ${JSON.stringify(requiredConfirm)} exactly -- pass --confirm ${requiredConfirm} once you've reviewed this transaction`);
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
let freshPlan;
|
|
2621
|
+
try {
|
|
2622
|
+
freshPlan = await replanTransaction(root, txn);
|
|
2623
|
+
} catch (err) {
|
|
2624
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', err.message);
|
|
2625
|
+
}
|
|
2626
|
+
const updated = await applyTransaction(root, flags.feature, flags.transaction, freshPlan, getPatchKind(txn.kind).apply);
|
|
2627
|
+
|
|
2628
|
+
const evidence = { transaction_id: updated.transaction_id, kind: updated.kind, applied_at: updated.apply.at };
|
|
2629
|
+
const gateState = passNamedGate(root, 'patch_transactions', flags.feature, evidence);
|
|
2630
|
+
|
|
2631
|
+
console.log(flags.json ? JSON.stringify({ transaction: updated, gate: gateState.gates.patch_transactions }, null, 2) : `applied: ${flags.transaction} [${updated.kind}]`);
|
|
2632
|
+
process.exit(0);
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
async function cmdPatchRollback(args) {
|
|
2636
|
+
const flags = parseCommand('patch rollback', args);
|
|
2637
|
+
if (flags.help) { console.log(renderCommandHelp('patch rollback')); process.exit(0); }
|
|
2638
|
+
setContext('patch rollback', flags);
|
|
2639
|
+
const root = requireRepoRoot();
|
|
2640
|
+
requirePreflightPassed(root);
|
|
2641
|
+
requireValidFeatureId(flags.feature);
|
|
2642
|
+
if (!flags.reason || !flags.reason.trim()) {
|
|
2643
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', 'bskel patch rollback requires --reason "..." -- every rollback must be auditable');
|
|
2644
|
+
}
|
|
2645
|
+
const txn = requireTransactionOrExit(root, flags.feature, flags.transaction);
|
|
2646
|
+
const updated = await rollbackTransaction(root, flags.feature, flags.transaction, flags.reason, { force: flags.force }, getPatchKind(txn.kind).rollback);
|
|
2647
|
+
|
|
2648
|
+
// Re-passing (not revoking) mirrors cmdScanCrossFeatureWaive's own precedent for this exact
|
|
2649
|
+
// situation -- recompute() only loops status:'applied' transactions, so a rolled-back one
|
|
2650
|
+
// naturally drops out of the gate's own input set on the next pass; a fresh pass here reflects
|
|
2651
|
+
// "still fine, just smaller" immediately rather than leaving the gate to be reactively
|
|
2652
|
+
// discovered stale by the next `bskel verify`.
|
|
2653
|
+
const evidence = { transaction_id: updated.transaction_id, kind: updated.kind, rolled_back_at: updated.rollback.at };
|
|
2654
|
+
const gateState = passNamedGate(root, 'patch_transactions', flags.feature, evidence);
|
|
2655
|
+
|
|
2656
|
+
console.log(flags.json ? JSON.stringify({ transaction: updated, gate: gateState.gates.patch_transactions }, null, 2) : `rolled back: ${flags.transaction} [${updated.kind}]`);
|
|
2657
|
+
process.exit(0);
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
// A deliberately-omitted-until-now, easy read-only convenience (D-patch-transactions' own EXIT
|
|
2661
|
+
// list named it explicitly) -- mirrors `dependency list`/`contract history`'s own pure-reader
|
|
2662
|
+
// posture, gate-independent like both.
|
|
2663
|
+
function cmdPatchList(args) {
|
|
2664
|
+
const flags = parseCommand('patch list', args);
|
|
2665
|
+
if (flags.help) { console.log(renderCommandHelp('patch list')); process.exit(0); }
|
|
2666
|
+
setContext('patch list', flags);
|
|
2667
|
+
const root = requireRepoRoot();
|
|
2668
|
+
requireValidFeatureId(flags.feature);
|
|
2669
|
+
const transactions = listTransactions(root, flags.feature);
|
|
2670
|
+
|
|
2671
|
+
if (flags.json) {
|
|
2672
|
+
console.log(JSON.stringify({ feature: flags.feature, transactions }, null, 2));
|
|
2673
|
+
} else if (!flags.quiet) {
|
|
2674
|
+
console.log(`patch transactions -- feature ${flags.feature}`);
|
|
2675
|
+
for (const t of transactions) {
|
|
2676
|
+
console.log(` ${t.transaction_id} [${t.status}] ${describePatchTransaction(t)}`);
|
|
2677
|
+
}
|
|
2678
|
+
if (transactions.length === 0) console.log(' (none proposed)');
|
|
2679
|
+
}
|
|
2680
|
+
process.exit(0);
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2082
2683
|
// O7 (D-handle-audit-report): a pure reader, deliberately gate-independent -- matches
|
|
2083
2684
|
// D-contract-history/D-gate-export's own posture, not `handles plan`/`handles emit`'s capability
|
|
2084
2685
|
// gating. It never touches adapter-specific codegen (the query is over `feature_uid` alone, the
|
|
@@ -2209,8 +2810,23 @@ function cmdObserveEmit(args) {
|
|
|
2209
2810
|
} catch (err) {
|
|
2210
2811
|
fail(EXIT_CODES.NOT_PASSED, 'PLAN_FAILED', err.message);
|
|
2211
2812
|
}
|
|
2813
|
+
} else if (scanReport.adapter === 'typescript-express') {
|
|
2814
|
+
// typescript-express's own project-root detection needs a module to anchor itself too (same
|
|
2815
|
+
// reasoning as python-fastapi's own --module dependency) -- plan.mjs's detectProjectRoot()
|
|
2816
|
+
// walks up from the module's own scanned files.
|
|
2817
|
+
let tsPlan;
|
|
2818
|
+
try {
|
|
2819
|
+
tsPlan = planTypeScriptExpress({ repoRoot: root, scanReport, module: flags.module, resourceFilter: null });
|
|
2820
|
+
} catch (err) {
|
|
2821
|
+
fail(EXIT_CODES.NOT_PASSED, 'PLAN_FAILED', err.message);
|
|
2822
|
+
}
|
|
2823
|
+
try {
|
|
2824
|
+
result = emitObserveTypeScriptExpress({ repoRoot: root, featureId: flags.feature, contract, plan: tsPlan, force: flags.force, reason: flags.reason, dryRun, computeDiff: flags.diff });
|
|
2825
|
+
} catch (err) {
|
|
2826
|
+
fail(EXIT_CODES.NOT_PASSED, 'PLAN_FAILED', err.message);
|
|
2827
|
+
}
|
|
2212
2828
|
} else {
|
|
2213
|
-
fail(EXIT_CODES.MISSING_CAPABILITY, 'MISSING_CAPABILITY', `bskel observe emit does not support the "${scanReport.adapter}" adapter yet (supported: java-spring, python-fastapi)`);
|
|
2829
|
+
fail(EXIT_CODES.MISSING_CAPABILITY, 'MISSING_CAPABILITY', `bskel observe emit does not support the "${scanReport.adapter}" adapter yet (supported: java-spring, python-fastapi, typescript-express)`);
|
|
2214
2830
|
}
|
|
2215
2831
|
const { written, conflicts, orphans, notes, forced, blocked, actions, postEmitNotes = [] } = result;
|
|
2216
2832
|
const wouldChange = actions.some((a) => a.action !== 'unchanged' && a.action !== 'adopt-unchanged');
|
|
@@ -2361,11 +2977,20 @@ function cmdObserveImport(args) {
|
|
|
2361
2977
|
const reportPath = specPath(root, flags.feature, 'observe', `${flags.feature}.conformance-report.json`);
|
|
2362
2978
|
writeFileAtomic(reportPath, `${JSON.stringify(report, null, 2)}\n`);
|
|
2363
2979
|
|
|
2364
|
-
// Evidence-first, not verdict-first (same as `contract`'s own precedent: a `partial`
|
|
2365
|
-
// is still passable via waiver) -- passes on a successful STRUCTURAL import, never on
|
|
2366
|
-
// violations found"
|
|
2367
|
-
//
|
|
2368
|
-
|
|
2980
|
+
// Evidence-first, not verdict-first by DEFAULT (same as `contract`'s own precedent: a `partial`
|
|
2981
|
+
// contract is still passable via waiver) -- passes on a successful STRUCTURAL import, never on
|
|
2982
|
+
// "zero violations found", unless --fail-on-violation opts into the stricter behavior. Whether
|
|
2983
|
+
// violation counts should block CI was a policy question deliberately left undecided in v1 --
|
|
2984
|
+
// this is that v1.1 layer, see D-runtime-conformance-receipts's own "Continued" entry in
|
|
2985
|
+
// DECISIONS.md. `awaitNamedGateDisposition` is the EXACT same disposition mechanism `contract`
|
|
2986
|
+
// already uses for "evidence exists but isn't good enough to pass silently" -- resolved by the
|
|
2987
|
+
// already-generic `bskel gate force conformance --feature <id> --reason "..."` (no new CLI verb
|
|
2988
|
+
// needed; `bskel next`'s own generic awaitingDispositionCommand() fallback already names it).
|
|
2989
|
+
const evidence = { receipt_count: receipts.length, matched, violations: violationCount };
|
|
2990
|
+
const blocking = flags['fail-on-violation'] && violationCount > 0;
|
|
2991
|
+
const gateState = blocking
|
|
2992
|
+
? awaitNamedGateDisposition(root, 'conformance', flags.feature, evidence)
|
|
2993
|
+
: passNamedGate(root, 'conformance', flags.feature, evidence);
|
|
2369
2994
|
|
|
2370
2995
|
if (flags.json) {
|
|
2371
2996
|
console.log(JSON.stringify({ report, noise_lines: noiseLines, gate: gateState.gates.conformance }, null, 2));
|
|
@@ -2374,8 +2999,11 @@ function cmdObserveImport(args) {
|
|
|
2374
2999
|
console.log(`${violationCount} violation(s), ${unsupportedCount} unsupported field(s) across matched receipts`);
|
|
2375
3000
|
console.log(`wrote ${path.relative(root, reportPath)}`);
|
|
2376
3001
|
console.log(`gate: conformance -> ${gateState.gates.conformance.status}`);
|
|
3002
|
+
if (blocking) {
|
|
3003
|
+
console.error(`\nblocked: ${violationCount} violation(s) found (--fail-on-violation) -- review ${path.relative(root, reportPath)}, then \`bskel gate force conformance --feature ${flags.feature} --reason "..."\` once accounted for.`);
|
|
3004
|
+
}
|
|
2377
3005
|
}
|
|
2378
|
-
process.exit(
|
|
3006
|
+
process.exit(blocking ? EXIT.AWAITING_DISPOSITION : EXIT.PASS);
|
|
2379
3007
|
}
|
|
2380
3008
|
|
|
2381
3009
|
// S2: "stale" alone sends a human/agent re-running steps until one happens to stick. Name the
|
|
@@ -2400,10 +3028,17 @@ function renderVerifyReport({ featureId, gates, artifacts, conflicts, build, all
|
|
|
2400
3028
|
const completenessNote = g.gate === 'contract' && evidence?.completeness
|
|
2401
3029
|
? ` (${evidence.completeness}${evidence.waived_count ? `: ${evidence.waived_count} waived` : ''})`
|
|
2402
3030
|
: '';
|
|
3031
|
+
// D-runtime-conformance-receipts (Continued, --fail-on-violation): same "surface the evidence
|
|
3032
|
+
// right in the verify report" precedent as `completenessNote` above -- fires for both `pass`
|
|
3033
|
+
// and `awaiting_disposition` states, since a human should see the count even when it didn't
|
|
3034
|
+
// end up blocking (i.e. --fail-on-violation wasn't used at import time).
|
|
3035
|
+
const conformanceNote = g.gate === 'conformance' && evidence?.violations
|
|
3036
|
+
? ` (${evidence.violations} violation(s), ${evidence.matched}/${evidence.receipt_count} matched)`
|
|
3037
|
+
: '';
|
|
2403
3038
|
// S4 (D-gate-history): a revoked gate's reason is exactly the kind of "why is this
|
|
2404
3039
|
// blocking" detail describeStale() already surfaces for stale gates -- same treatment here.
|
|
2405
3040
|
const revokedNote = g.status === 'revoked' && g.record?.reason ? ` (revoked: ${g.record.reason})` : '';
|
|
2406
|
-
lines.push(`- [${marker}] ${g.gate}${suffix}${completenessNote}${describeStale(g)}${revokedNote}`);
|
|
3041
|
+
lines.push(`- [${marker}] ${g.gate}${suffix}${completenessNote}${conformanceNote}${describeStale(g)}${revokedNote}`);
|
|
2407
3042
|
}
|
|
2408
3043
|
lines.push('', '## Artifacts');
|
|
2409
3044
|
for (const a of artifacts) lines.push(`- [${a.exists ? 'OK' : 'MISSING'}] ${a.artifact}: ${a.path}`);
|
|
@@ -2603,6 +3238,66 @@ function cmdDoctor(args) {
|
|
|
2603
3238
|
process.exit(allOk ? 0 : 1);
|
|
2604
3239
|
}
|
|
2605
3240
|
|
|
3241
|
+
// D-http-serving-layer: starts a real, long-running HTTP server (lib/http-server.mjs) -- unlike
|
|
3242
|
+
// every other command in this file, success here does NOT process.exit(); the server's own open
|
|
3243
|
+
// socket keeps the event loop alive until Ctrl+C (SIGINT) or SIGTERM. Every route handler calls
|
|
3244
|
+
// straight into the same lib/ functions the CLI commands use -- no separate business logic lives in
|
|
3245
|
+
// the HTTP layer itself.
|
|
3246
|
+
async function cmdServe(args) {
|
|
3247
|
+
const flags = parseCommand('serve', args);
|
|
3248
|
+
if (flags.help) { console.log(renderCommandHelp('serve')); process.exit(0); }
|
|
3249
|
+
setContext('serve', flags);
|
|
3250
|
+
const root = requireRepoRoot();
|
|
3251
|
+
const port = Number.parseInt(flags.port, 10);
|
|
3252
|
+
|
|
3253
|
+
// D-ddl-apply: eagerly checked here, at startup -- the DB-touching routes must not silently
|
|
3254
|
+
// 404 forever because of a typo'd env var name; missing/unset is BAD_ARGS at the CLI boundary,
|
|
3255
|
+
// same convention as resolveDbSchemaOrExit's own `bskel scan --db` handling. The live
|
|
3256
|
+
// connection itself still only ever opens lazily, per-request (no eager connect-at-startup, no
|
|
3257
|
+
// pool) -- a transient DB outage must not block the unrelated, DB-independent parts of the UI.
|
|
3258
|
+
let dbConfig = null;
|
|
3259
|
+
if (flags['database-url-env']) {
|
|
3260
|
+
if (!process.env[flags['database-url-env']]) {
|
|
3261
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', `--database-url-env ${flags['database-url-env']} names an environment variable that isn't set -- export it first (never read from .env directly; see D-db-schema-plane in DECISIONS.md)`);
|
|
3262
|
+
}
|
|
3263
|
+
dbConfig = { databaseUrlEnv: flags['database-url-env'], schema: flags.schema, signKeyPath: flags['sign-key'] };
|
|
3264
|
+
// D-ddl-apply: closes this feature's own named "mandatory signing... cheap, well-justified
|
|
3265
|
+
// near-term addition" EXIT item -- an opt-in-to-MORE-strictness knob, letting a cautious team
|
|
3266
|
+
// enforce mandatory signing for themselves without this tool forcing it on every user. A
|
|
3267
|
+
// no-op when --database-url-env wasn't given at all (this whole block never runs then) --
|
|
3268
|
+
// nothing to enforce on a DDL surface that isn't running.
|
|
3269
|
+
if (flags['require-sign-key'] && !dbConfig.signKeyPath) {
|
|
3270
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', '--require-sign-key was given but --sign-key was not -- refusing to start the DDL surface without an audit-signing key. Pass --sign-key <path>, or drop --require-sign-key to allow starting unsigned (with a warning).');
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
let started;
|
|
3275
|
+
try {
|
|
3276
|
+
started = await createHttpServer(root, { host: flags.host, port, dbConfig });
|
|
3277
|
+
} catch (err) {
|
|
3278
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', `could not start server: ${err.message}`);
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
if (flags.json) {
|
|
3282
|
+
console.log(JSON.stringify({ listening: started.url, host: started.host, port: started.port, repo: root, ddlApplyEnabled: Boolean(dbConfig) }));
|
|
3283
|
+
} else if (!flags.quiet) {
|
|
3284
|
+
console.log(`bskel serve -- listening on ${started.url}`);
|
|
3285
|
+
console.log(` UI: ${started.url}/`);
|
|
3286
|
+
console.log(` API: ${started.url}/api/...`);
|
|
3287
|
+
if (dbConfig) {
|
|
3288
|
+
console.log(` DDL apply routes: ENABLED (schema "${dbConfig.schema}", connection from $${dbConfig.databaseUrlEnv})`);
|
|
3289
|
+
if (!dbConfig.signKeyPath) {
|
|
3290
|
+
console.log(' WARNING: no --sign-key given -- every propose/approve/apply is recorded in specs/ but NOT cryptographically attested. Pass --sign-key <path> to change this.');
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
console.log('press Ctrl+C to stop');
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
const shutdown = () => started.server.close(() => process.exit(0));
|
|
3297
|
+
process.on('SIGINT', shutdown);
|
|
3298
|
+
process.on('SIGTERM', shutdown);
|
|
3299
|
+
}
|
|
3300
|
+
|
|
2606
3301
|
// P2 (D-greenfield-bootstrap): the one path into this tool that doesn't require an existing git
|
|
2607
3302
|
// repo (contrast requireRepoRoot(), used by nearly everything else) -- `bskel new` is what CREATES
|
|
2608
3303
|
// one. `--stack`'s two choices come from new/index.mjs's plain dispatch map, not a dynamic
|
|
@@ -2823,6 +3518,8 @@ async function dispatchCommand(cmd, rest) {
|
|
|
2823
3518
|
case 'scan': {
|
|
2824
3519
|
if (rest[0] === 'disposition') return cmdScanDisposition(rest.slice(1));
|
|
2825
3520
|
if (rest[0] === 'explain') return cmdScanExplain(rest.slice(1));
|
|
3521
|
+
if (rest[0] === 'cross-feature-check') return cmdScanCrossFeatureCheck(rest.slice(1));
|
|
3522
|
+
if (rest[0] === 'cross-feature-waive') return cmdScanCrossFeatureWaive(rest.slice(1));
|
|
2826
3523
|
await cmdScan(rest);
|
|
2827
3524
|
break;
|
|
2828
3525
|
}
|
|
@@ -2850,12 +3547,32 @@ async function dispatchCommand(cmd, rest) {
|
|
|
2850
3547
|
process.exit(14);
|
|
2851
3548
|
break;
|
|
2852
3549
|
}
|
|
3550
|
+
case 'dependency': {
|
|
3551
|
+
const sub = rest[0];
|
|
3552
|
+
const subArgs = rest.slice(1);
|
|
3553
|
+
if (sub === 'declare') return cmdDependencyDeclare(subArgs);
|
|
3554
|
+
if (sub === 'remove') return cmdDependencyRemove(subArgs);
|
|
3555
|
+
if (sub === 'list') return cmdDependencyList(subArgs);
|
|
3556
|
+
usage();
|
|
3557
|
+
process.exit(14);
|
|
3558
|
+
break;
|
|
3559
|
+
}
|
|
2853
3560
|
case 'stack': {
|
|
2854
3561
|
if (rest[0] === 'apply') return cmdStackApply(rest.slice(1));
|
|
2855
3562
|
usage();
|
|
2856
3563
|
process.exit(14);
|
|
2857
3564
|
break;
|
|
2858
3565
|
}
|
|
3566
|
+
case 'patch': {
|
|
3567
|
+
if (rest[0] === 'propose') return cmdPatchPropose(rest.slice(1));
|
|
3568
|
+
if (rest[0] === 'approve') return cmdPatchApprove(rest.slice(1));
|
|
3569
|
+
if (rest[0] === 'apply') return cmdPatchApply(rest.slice(1));
|
|
3570
|
+
if (rest[0] === 'rollback') return cmdPatchRollback(rest.slice(1));
|
|
3571
|
+
if (rest[0] === 'list') return cmdPatchList(rest.slice(1));
|
|
3572
|
+
usage();
|
|
3573
|
+
process.exit(14);
|
|
3574
|
+
break;
|
|
3575
|
+
}
|
|
2859
3576
|
case 'catalog': {
|
|
2860
3577
|
if (rest[0] === 'lint') return cmdCatalogLint(rest.slice(1));
|
|
2861
3578
|
usage();
|
|
@@ -2900,9 +3617,18 @@ async function dispatchCommand(cmd, rest) {
|
|
|
2900
3617
|
process.exit(14);
|
|
2901
3618
|
break;
|
|
2902
3619
|
}
|
|
3620
|
+
case 'attest': {
|
|
3621
|
+
if (rest[0] === 'keygen') return cmdAttestKeygen(rest.slice(1));
|
|
3622
|
+
if (rest[0] === 'verify') return cmdAttestVerify(rest.slice(1));
|
|
3623
|
+
usage();
|
|
3624
|
+
process.exit(14);
|
|
3625
|
+
break;
|
|
3626
|
+
}
|
|
2903
3627
|
case 'doctor':
|
|
2904
3628
|
cmdDoctor(rest);
|
|
2905
3629
|
break;
|
|
3630
|
+
case 'serve':
|
|
3631
|
+
return cmdServe(rest);
|
|
2906
3632
|
case 'new':
|
|
2907
3633
|
return cmdNew(rest);
|
|
2908
3634
|
default:
|