@mnstry/atelier 0.2.0-alpha.13 → 0.2.0-alpha.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +283 -0
- package/README.md +102 -52
- package/SECURITY.md +1 -1
- package/TRADEMARKS.md +2 -2
- package/contracts/atelier-extension-manifest.v1.schema.json +412 -0
- package/contracts/atelier-graph-view.v1.schema.json +1085 -0
- package/contracts/public-api-baseline.json +298 -19
- package/docs/README.md +90 -0
- package/docs/blocks/capability-stewardship.md +1 -1
- package/docs/blocks/claims.md +2 -2
- package/docs/blocks/conformance-admission.md +1 -1
- package/docs/blocks/learning-harnesses.md +1 -1
- package/docs/{atelier-sync.md → guides/atelier-sync.md} +20 -14
- package/docs/{capability-stewardship.md → guides/capability-stewardship.md} +2 -4
- package/docs/guides/co-authoring.md +113 -0
- package/docs/{distributions.md → guides/distributions.md} +13 -8
- package/docs/guides/extensions.md +189 -0
- package/docs/guides/graph-refresh.md +152 -0
- package/docs/guides/graph-workspace.md +64 -0
- package/docs/{ingestion.md → guides/ingestion.md} +16 -15
- package/docs/{inquiry-harnesses.md → guides/inquiry-harnesses.md} +2 -3
- package/docs/{knowledge-workspace.md → guides/knowledge-workspace.md} +2 -2
- package/docs/{learning-harnesses.md → guides/learning-harnesses.md} +2 -3
- package/docs/{learning.md → guides/learning.md} +12 -5
- package/docs/{obsidian.md → guides/obsidian.md} +370 -23
- package/docs/guides/ontology-guide.md +111 -0
- package/docs/{personal-workspace.md → guides/personal-workspace.md} +17 -5
- package/docs/{research-starter.md → guides/research-starter.md} +1 -1
- package/docs/{tenant-readiness.md → guides/tenant-readiness.md} +39 -0
- package/docs/{upgrade.md → guides/upgrade.md} +197 -11
- package/docs/knowledge-health/assessment/capabilities.md +6 -15
- package/docs/knowledge-health/facilitator.md +15 -4
- package/docs/knowledge-health/{devday/participant-response.md → participant-response.md} +4 -2
- package/docs/knowledge-health/quickstart.md +97 -75
- package/docs/knowledge-health/readme.md +13 -5
- package/docs/maintainers/describing-the-atelier.md +173 -0
- package/docs/{release-engineering.md → maintainers/release-engineering.md} +5 -4
- package/docs/{announcements.md → reference/announcements.md} +1 -1
- package/docs/{architecture.md → reference/architecture.md} +31 -5
- package/docs/{atelier-runtime.md → reference/atelier-runtime.md} +1 -1
- package/docs/{connected-composition.md → reference/connected-composition.md} +7 -10
- package/docs/{coordination.md → reference/coordination.md} +1 -1
- package/docs/{decisions.md → reference/decisions.md} +3 -3
- package/docs/{design.md → reference/design.md} +1 -1
- package/docs/reference/edge-policy.md +151 -0
- package/docs/reference/graph-view-structured.md +119 -0
- package/docs/reference/graph-view.md +139 -0
- package/docs/reference/graph-workspace.md +37 -0
- package/docs/{intake-and-guides.md → reference/intake-and-guides.md} +3 -4
- package/docs/{knowledge-graph.md → reference/knowledge-graph.md} +120 -6
- package/docs/{local-services.md → reference/local-services.md} +43 -4
- package/docs/{obsidian-contract.md → reference/obsidian-contract.md} +29 -4
- package/docs/{obsidian-plugin.md → reference/obsidian-plugin.md} +218 -68
- package/docs/{practical-judgment.md → reference/phronesis.md} +12 -3
- package/docs/reference/portability.md +92 -0
- package/docs/{portable-presentation.md → reference/portable-presentation.md} +2 -2
- package/docs/{presentation-consumer-boundaries.md → reference/presentation-consumer-boundaries.md} +5 -0
- package/docs/{project-options.md → reference/project-options.md} +17 -0
- package/docs/{repo-boundary-guard.md → reference/repo-boundary-guard.md} +50 -4
- package/docs/{review-portability.md → reference/review-portability.md} +4 -0
- package/docs/{semantic-operation.md → reference/semantic-operation.md} +4 -3
- package/docs/{template-profile.md → reference/template-profile.md} +1 -1
- package/docs/{trackables.md → reference/trackables.md} +2 -2
- package/docs/start/independent-start.md +89 -0
- package/docs/{install.md → start/install.md} +9 -9
- package/docs/{knowledge-setup.md → start/knowledge-setup.md} +5 -5
- package/docs/{research-starter-quickstart.md → start/research-starter-quickstart.md} +6 -6
- package/fixtures/atelier-architecture/catalog/invalid/document.json +2 -2
- package/fixtures/atelier-architecture/catalog/valid/document.json +2 -2
- package/fixtures/graph-view/synthetic-authorized-input.v1.json +437 -0
- package/fixtures/graph-view/synthetic-view.v1.json +1 -0
- package/fixtures/knowledge-health/workshop/README.md +1 -1
- package/fixtures/templates/local-library.v1.json +3 -1
- package/package.json +22 -8
- package/plugins/obsidian/main.js +57 -8
- package/plugins/obsidian/manifest.json +1 -1
- package/skills/claude/atelier-guided-upgrade/SKILL.md +1 -1
- package/skills/claude/atelier-knowledge-setup/SKILL.md +2 -2
- package/skills/claude/atelier-local-service/SKILL.md +1 -1
- package/skills/claude/atelier-practical-judgment/SKILL.md +7 -2
- package/skills/claude/atelier-skill-steward/SKILL.md +1 -1
- package/skills/claude/mnstry-readiness/SKILL.md +2 -2
- package/skills/codex/atelier-guided-upgrade/SKILL.md +1 -1
- package/skills/codex/atelier-knowledge-setup/SKILL.md +2 -2
- package/skills/codex/atelier-local-service/SKILL.md +1 -1
- package/skills/codex/atelier-practical-judgment/SKILL.md +7 -2
- package/skills/codex/atelier-skill-steward/SKILL.md +1 -1
- package/skills/codex/mnstry-readiness/SKILL.md +2 -2
- package/src/architecture/catalog.mjs +2 -2
- package/src/attestation/sign.mjs +1 -1
- package/src/boundary/content-rules.mjs +534 -68
- package/src/boundary/policy.mjs +187 -60
- package/src/cli/run.mjs +26 -9
- package/src/commands/distribution.mjs +1 -1
- package/src/commands/extension.mjs +27 -0
- package/src/commands/ingest.mjs +1 -1
- package/src/commands/learn.mjs +1 -1
- package/src/commands/obsidian.mjs +49 -21
- package/src/commands/review.mjs +3 -0
- package/src/edge-policy/contract.mjs +69 -0
- package/src/edge-policy/policy.mjs +142 -0
- package/src/edge-policy/source-citation-adapter.mjs +43 -0
- package/src/extension-packs/loader.mjs +4 -3
- package/src/extensions/admission.mjs +83 -0
- package/src/extensions/client.d.mts +23 -0
- package/src/extensions/client.mjs +99 -0
- package/src/extensions/contracts.d.mts +29 -0
- package/src/extensions/contracts.mjs +163 -0
- package/src/extensions/convex.d.mts +24 -0
- package/src/extensions/convex.mjs +189 -0
- package/src/extensions/delegation.mjs +54 -0
- package/src/extensions/drizzle.d.mts +11 -0
- package/src/extensions/drizzle.mjs +33 -0
- package/src/extensions/postgres.d.mts +7 -0
- package/src/extensions/postgres.mjs +66 -0
- package/src/extensions/prisma.d.mts +13 -0
- package/src/extensions/prisma.mjs +63 -0
- package/src/extensions/project.mjs +432 -0
- package/src/extensions/scaffold-convex.mjs +130 -0
- package/src/extensions/scaffold-sql.mjs +144 -0
- package/src/extensions/server.d.mts +20 -0
- package/src/extensions/server.mjs +102 -0
- package/src/extensions/sql-scope.mjs +22 -0
- package/src/graph/graph.mjs +13 -5
- package/src/graph/knowledge-graph.mjs +20 -2
- package/src/graph/repo-contribution.mjs +405 -0
- package/src/graph/repo-contribution.types.mjs +32 -0
- package/src/graph-access/edge-decision-adapter.mjs +41 -0
- package/src/graph-access/edge-record-adapter.mjs +73 -0
- package/src/graph-access/index.mjs +253 -0
- package/src/graph-access/multi-contribution-record-adapter.mjs +101 -0
- package/src/graph-access/multi-record-projection-adapter.mjs +74 -0
- package/src/graph-access/source-adapter.mjs +81 -0
- package/src/graph-refresh/compare.mjs +110 -0
- package/src/graph-refresh/controller.mjs +324 -0
- package/src/graph-refresh/index.mjs +3 -0
- package/src/graph-refresh/local.mjs +78 -0
- package/src/graph-view/babylon/babylon-scene-port.mjs +64 -0
- package/src/graph-view/babylon/index.mjs +2 -0
- package/src/graph-view/babylon/navigator.mjs +312 -0
- package/src/graph-view/babylon/provenance.mjs +36 -0
- package/src/graph-view/babylon/scene-controller.mjs +131 -0
- package/src/graph-view/babylon/semantic-picking.mjs +35 -0
- package/src/graph-view/babylon/web-host.mjs +188 -0
- package/src/graph-view/cli.mjs +3 -0
- package/src/graph-view/command.mjs +34 -0
- package/src/graph-view/contracts.mjs +79 -0
- package/src/graph-view/errors.mjs +5 -0
- package/src/graph-view/index.mjs +4 -0
- package/src/graph-view/intents.mjs +30 -0
- package/src/graph-view/query.mjs +213 -0
- package/src/graph-view/refs.mjs +2 -0
- package/src/graph-view/structured/browser.mjs +262 -0
- package/src/graph-view/structured/styles.mjs +8 -0
- package/src/graph-workspace/authoring.mjs +185 -0
- package/src/graph-workspace/browser/constellation.mjs +369 -0
- package/src/graph-workspace/browser/dom.mjs +23 -0
- package/src/graph-workspace/browser/index.mjs +6 -0
- package/src/graph-workspace/browser/review.mjs +55 -0
- package/src/graph-workspace/browser/styles.mjs +11 -0
- package/src/graph-workspace/browser/workbench.mjs +351 -0
- package/src/graph-workspace/browser/workspace.mjs +431 -0
- package/src/graph-workspace/disclosure.mjs +39 -0
- package/src/graph-workspace/geometry.mjs +78 -0
- package/src/graph-workspace/index.mjs +11 -0
- package/src/graph-workspace/labels.mjs +50 -0
- package/src/graph-workspace/model.mjs +109 -0
- package/src/graph-workspace/recipes.mjs +88 -0
- package/src/graph-workspace/scale.mjs +95 -0
- package/src/graph-workspace/theme.mjs +156 -0
- package/src/ingestion/optional/graphify/graphify_supplier.py +5 -0
- package/src/ingestion/optional/graphify/native-closure-pins.json +275 -0
- package/src/ingestion/optional/graphify/native-pins.json +306 -0
- package/src/knowledge/ontology-guide.mjs +152 -0
- package/src/knowledge-health/proof/installed-check.mjs +2 -2
- package/src/knowledge-health/rule-identity.mjs +14 -9
- package/src/knowledge-health/workshop.mjs +22 -2
- package/src/personal-workspace/index.mjs +1 -1
- package/src/personal-workspace/selection.mjs +34 -3
- package/src/project/config.mjs +1 -1
- package/src/project/path-match.mjs +2 -1
- package/src/project/providers/github.mjs +24 -0
- package/src/project/repo-identity.mjs +3 -22
- package/src/projection/obsidian/contracts.mjs +1 -1
- package/src/projection/obsidian/materialize/path-registry.mjs +1 -1
- package/src/projection/obsidian/materialize/prepare-view.mjs +88 -6
- package/src/projection/obsidian/materialize/redaction.mjs +1 -1
- package/src/projection/obsidian/personal-workspace.mjs +84 -5
- package/src/projection/obsidian/plugin-bridge/channel.mjs +1 -1
- package/src/projection/obsidian/proposals/adapter.mjs +46 -23
- package/src/projection/obsidian/proposals/observation.mjs +40 -2
- package/src/projection/obsidian/publication/publisher.mjs +96 -8
- package/src/projection/project.mjs +21 -16
- package/src/readiness/readiness.mjs +20 -11
- package/src/readiness-protocols/bundled-pack.mjs +14 -766
- package/src/readiness-protocols/bundled-selection.mjs +32 -0
- package/src/readiness-protocols/evidence.mjs +2 -1
- package/src/readiness-protocols/packs/mnstry.mjs +766 -0
- package/src/readiness-protocols/runtime.mjs +43 -6
- package/src/runtime/obsidian/engine.mjs +387 -76
- package/src/runtime/obsidian/lifecycle.mjs +20 -15
- package/src/runtime/obsidian/login-item.mjs +6 -3
- package/src/runtime/obsidian/machine-settings.mjs +52 -8
- package/src/runtime/obsidian/observation.mjs +36 -4
- package/src/runtime/obsidian/opening.mjs +4 -3
- package/src/runtime/obsidian/personal-selection.mjs +187 -0
- package/src/runtime/obsidian/pipeline.mjs +115 -6
- package/src/runtime/obsidian/private-lock.mjs +50 -6
- package/src/runtime/obsidian/selection-continuity.mjs +124 -0
- package/src/runtime/obsidian/service-main.mjs +68 -16
- package/src/runtime/obsidian/service.mjs +80 -6
- package/src/runtime/obsidian/unheld-evidence.mjs +1 -1
- package/src/runtime/obsidian/view-permission.mjs +335 -0
- package/src/ui/control-system.mjs +201 -0
- package/src/ui/html-primitives.mjs +3 -0
- package/src/upgrade/closure-diagnostics.mjs +135 -0
- package/src/upgrade/closure.mjs +649 -0
- package/src/upgrade/upgrade.mjs +42 -11
- package/templates/distribution-workspace/README.md +2 -2
- package/templates/external-project-workspace/atelier.project.json +3 -0
- package/templates/external-project-workspace/pack-lifecycle.json +3 -1
- package/templates/knowledge-workspace/README.md +2 -2
- package/docs/integration-and-review-plan.md +0 -409
- package/docs/integration-contract-decisions.md +0 -44
- package/docs/knowledge-health/devday/facilitator.md +0 -16
- package/docs/knowledge-health/devday/public-default.md +0 -107
- package/docs/knowledge-health/devday/quickstart.md +0 -18
- package/docs/knowledge-health/devday/support.md +0 -22
- package/docs/knowledge-health/exercise.md +0 -17
- package/docs/knowledge-health/support.md +0 -34
- package/docs/presentation-review-disposition.md +0 -47
- package/docs/progressive-ingestion-acceptance.md +0 -89
- package/docs/progressive-ingestion.md +0 -235
- /package/docs/{exact-upgrades.md → guides/exact-upgrades.md} +0 -0
- /package/docs/{guided-upgrades.md → guides/guided-upgrades.md} +0 -0
- /package/docs/{local-review.md → guides/local-review.md} +0 -0
- /package/docs/{skill-steward.md → guides/skill-steward.md} +0 -0
- /package/docs/{template-upgrades.md → guides/template-upgrades.md} +0 -0
- /package/docs/{assurance-controls.md → maintainers/assurance-controls.md} +0 -0
- /package/docs/{attestation.md → reference/attestation.md} +0 -0
- /package/docs/{coauthor-session.md → reference/coauthor-session.md} +0 -0
- /package/docs/{continuity.md → reference/continuity.md} +0 -0
- /package/docs/{contract-stability.md → reference/contract-stability.md} +0 -0
- /package/docs/{decision-practices.md → reference/decision-practices.md} +0 -0
- /package/docs/{evidence-navigation.md → reference/evidence-navigation.md} +0 -0
- /package/docs/{extension-pack-lifecycle.md → reference/extension-pack-lifecycle.md} +0 -0
- /package/docs/{interaction-and-reflection.md → reference/interaction-and-reflection.md} +0 -0
- /package/docs/{local-state.md → reference/local-state.md} +0 -0
- /package/docs/{ontology.md → reference/ontology.md} +0 -0
- /package/docs/{optional-ingestion.md → reference/optional-ingestion.md} +0 -0
- /package/docs/{project-command-inventory.md → reference/project-command-inventory.md} +0 -0
- /package/docs/{semantic-ingestion.md → reference/semantic-ingestion.md} +0 -0
- /package/docs/{vault-service.md → reference/vault-service.md} +0 -0
package/src/boundary/policy.mjs
CHANGED
|
@@ -7,6 +7,9 @@ import { matchesPathPattern } from '../project/path-match.mjs'
|
|
|
7
7
|
import { sanitizedGitEnvironment } from '../runtime/git-adapter.mjs'
|
|
8
8
|
import {
|
|
9
9
|
CHECK_AGGREGATE_MAX_BYTES,
|
|
10
|
+
canonicalDiffArgs,
|
|
11
|
+
gitOutputResult,
|
|
12
|
+
parseDiffSections,
|
|
10
13
|
parsePushRefInput,
|
|
11
14
|
resolveContentRules,
|
|
12
15
|
scanPushUpdate,
|
|
@@ -299,6 +302,19 @@ function finding({ severity = 'error', code, message, repo = null, path: itemPat
|
|
|
299
302
|
return { severity, code, message, repo, path: itemPath, node, details }
|
|
300
303
|
}
|
|
301
304
|
|
|
305
|
+
// Text output lists blocking errors before warnings, with incomplete or unresolved evidence first within each
|
|
306
|
+
// severity group. The report and --json keep every finding in their original order.
|
|
307
|
+
const PRINTED_FINDINGS_MAX = 50
|
|
308
|
+
const INCOMPLETE_SCAN_CODES = new Set(['content-scan-incomplete', 'semantic-field-scan-incomplete', 'repo-unscannable', 'pre-commit-repo-unresolved', 'push-check-repo-unresolved'])
|
|
309
|
+
|
|
310
|
+
function printedFindings(findings) {
|
|
311
|
+
const ordered = [true, false].flatMap((blocking) => {
|
|
312
|
+
const group = findings.filter((item) => (item.severity === 'error') === blocking)
|
|
313
|
+
return [...group.filter((item) => INCOMPLETE_SCAN_CODES.has(item.code)), ...group.filter((item) => !INCOMPLETE_SCAN_CODES.has(item.code))]
|
|
314
|
+
})
|
|
315
|
+
return { shown: ordered.slice(0, PRINTED_FINDINGS_MAX), hidden: Math.max(0, ordered.length - PRINTED_FINDINGS_MAX) }
|
|
316
|
+
}
|
|
317
|
+
|
|
302
318
|
function severityFor(policy) {
|
|
303
319
|
return policy?.mode === 'legacy-warning' ? 'warning' : 'error'
|
|
304
320
|
}
|
|
@@ -358,17 +374,90 @@ function actorFindings({ policy, project, actor, gitExecutable, allowNetworkActo
|
|
|
358
374
|
return findings
|
|
359
375
|
}
|
|
360
376
|
|
|
361
|
-
|
|
377
|
+
/**
|
|
378
|
+
* The index a pre-commit hook is judging. Git names it in GIT_INDEX_FILE (a
|
|
379
|
+
* temporary index under `git commit -a` or `git commit <paths>`, a linked
|
|
380
|
+
* worktree's own index, or `.git/index` relative to the work tree's top) and
|
|
381
|
+
* runs the hook at that top, though a chained hook may have moved. The work
|
|
382
|
+
* tree is found by Git's own discovery from where the hook runs; the hook's
|
|
383
|
+
* GIT_DIR is not used, because in a linked worktree Git exports it without a
|
|
384
|
+
* work tree, which would make any subdirectory look like the top. A work tree
|
|
385
|
+
* discovery cannot find is unresolved, and blocks. Returns null when no index
|
|
386
|
+
* is named.
|
|
387
|
+
*/
|
|
388
|
+
export function hookIndexFromEnvironment({ env = process.env, cwd = process.cwd(), gitExecutable = 'git' } = {}) {
|
|
389
|
+
const named = typeof env.GIT_INDEX_FILE === 'string' ? env.GIT_INDEX_FILE : ''
|
|
390
|
+
if (!named) return null
|
|
391
|
+
const workTree = gitPathOutput(cwd, ['rev-parse', '--show-toplevel'], sanitizedGitEnvironment(), gitExecutable)
|
|
392
|
+
const commonDir = gitPathOutput(cwd, ['rev-parse', '--git-common-dir'], sanitizedGitEnvironment(), gitExecutable)
|
|
393
|
+
return { workTree, indexFile: path.resolve(workTree ?? cwd, named), commonDir }
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function gitPathOutput(cwd, args, env, gitExecutable = 'git') {
|
|
397
|
+
const result = spawnSync(gitExecutable, ['-C', cwd, ...args], { encoding: 'utf8', env })
|
|
398
|
+
const output = result.status === 0 ? String(result.stdout ?? '').replace(/\r?\n$/, '') : ''
|
|
399
|
+
return output ? nativeRealPath(path.resolve(cwd, output)) : null
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// The file system's own spelling of a path, so a configured path that differs
|
|
403
|
+
// only in case (macOS, Windows) still names the same repo.
|
|
404
|
+
function nativeRealPath(value) {
|
|
405
|
+
try {
|
|
406
|
+
return fs.realpathSync.native(path.resolve(value))
|
|
407
|
+
} catch {
|
|
408
|
+
return path.resolve(value)
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Where each managed repo's staged state is read. The repo the hook is
|
|
414
|
+
* committing is read from the hook's work tree and index: the managed repo
|
|
415
|
+
* configured at that work tree, or the one managed repo sharing its Git
|
|
416
|
+
* directory (a linked worktree of it). Every other repo is read as configured.
|
|
417
|
+
* When an index is named but no single managed repo is being committed, the
|
|
418
|
+
* guard cannot tell what it is judging, and says so.
|
|
419
|
+
*/
|
|
420
|
+
function stagedTargets(project, hookIndex, gitExecutable = 'git') {
|
|
421
|
+
const repos = managedRepos(project)
|
|
422
|
+
const targets = new Map(repos.map((repo) => [repo, { root: repo.path, indexFile: null }]))
|
|
423
|
+
if (!hookIndex) return { targets, unresolved: null }
|
|
424
|
+
let committing = hookIndex.workTree ? repos.filter((repo) => repo.path && nativeRealPath(repo.path) === hookIndex.workTree) : []
|
|
425
|
+
if (committing.length === 0 && hookIndex.commonDir) {
|
|
426
|
+
committing = repos.filter((repo) => repo.path && fs.existsSync(repo.path)
|
|
427
|
+
&& gitPathOutput(repo.path, ['rev-parse', '--git-common-dir'], sanitizedGitEnvironment(), gitExecutable) === hookIndex.commonDir)
|
|
428
|
+
}
|
|
429
|
+
if (committing.length !== 1) return { targets, unresolved: hookIndex.workTree ?? '(an unresolved work tree)' }
|
|
430
|
+
targets.set(committing[0], { root: hookIndex.workTree, indexFile: hookIndex.indexFile })
|
|
431
|
+
return { targets, unresolved: null }
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function stagedPathsResult(targets, { gitExecutable = 'git' } = {}) {
|
|
362
435
|
const paths = []
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
436
|
+
const failures = []
|
|
437
|
+
for (const [repo, target] of targets) {
|
|
438
|
+
if (!target.root || !fs.existsSync(path.join(target.root, '.git'))) {
|
|
439
|
+
failures.push({ repo, reason: 'configured-path-absent-or-not-git' })
|
|
440
|
+
continue
|
|
441
|
+
}
|
|
442
|
+
// NUL-separated names are exact: a quoted name could never match a forbidden-path glob.
|
|
443
|
+
const result = gitOutputResult(target.root, ['diff', '--cached', '--name-only', '-z', '--no-relative', '--no-renames', '--diff-filter=ACMRT'], { gitExecutable, indexFile: target.indexFile, maxBuffer: CHECK_AGGREGATE_MAX_BYTES })
|
|
444
|
+
if (!result.ok || (result.stdout && !result.stdout.endsWith('\0'))) {
|
|
445
|
+
failures.push({ repo, reason: result.code ?? 'unterminated-staged-path-evidence' })
|
|
446
|
+
continue
|
|
447
|
+
}
|
|
448
|
+
for (const rel of result.stdout.split('\0').filter(Boolean)) {
|
|
449
|
+
paths.push({ repo: repo.name, repoRoot: target.root, path: normalize(rel) })
|
|
369
450
|
}
|
|
370
451
|
}
|
|
371
|
-
return paths
|
|
452
|
+
return { paths, failures }
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export function stagedPathsForProject(project, { gitExecutable = 'git', hookIndex = null } = {}) {
|
|
456
|
+
const { targets, unresolved } = stagedTargets(project, hookIndex, gitExecutable)
|
|
457
|
+
const result = stagedPathsResult(targets, { gitExecutable })
|
|
458
|
+
if (unresolved) throw new Error('the committing repository could not be resolved; staged paths are incomplete')
|
|
459
|
+
if (result.failures.length) throw new Error('staged paths could not be listed completely')
|
|
460
|
+
return result.paths
|
|
372
461
|
}
|
|
373
462
|
|
|
374
463
|
function forbiddenPathFindings({ policy, stagedPaths }) {
|
|
@@ -405,42 +494,48 @@ export function semanticFieldOnLine(line) {
|
|
|
405
494
|
return { field: null, value: null, raw: line.trim() }
|
|
406
495
|
}
|
|
407
496
|
|
|
497
|
+
// A hunk cannot establish the quote/comment state at its first line. Every literal boundary assignment therefore
|
|
498
|
+
// remains a review signal, including examples inside quoted text. A plain mention is not an assignment. Keep the
|
|
499
|
+
// whole suffix: trimming a partial collection or comment could misread it as the private initialization default.
|
|
500
|
+
function semanticFieldsOnLine(line) {
|
|
501
|
+
const raw = String(line).trim()
|
|
502
|
+
const values = []
|
|
503
|
+
const match = raw.match(/(?:^|[^\w.])["']?(kg\.audience|audience|handling|sensitivity|data_boundary)["']?\s*:/i)
|
|
504
|
+
if (match) values.push({ field: match[1].toLowerCase().replace(/^kg\./, ''), value: unquoteValue(raw.slice(match.index + match[0].length)), raw })
|
|
505
|
+
// Complete escaped JSON keys add signals without guessing the document context or changing the legacy helper.
|
|
506
|
+
for (const escaped of raw.matchAll(/("(?:\\.|[^"\\])*")\s*:/g)) {
|
|
507
|
+
if (!escaped[1].includes('\\')) continue
|
|
508
|
+
try {
|
|
509
|
+
const field = JSON.parse(escaped[1]).toLowerCase().replace(/^kg\./, '')
|
|
510
|
+
if (['audience', 'handling', 'sensitivity', 'data_boundary'].includes(field)) {
|
|
511
|
+
values.push({ field, value: unquoteValue(raw.slice(escaped.index + escaped[0].length)), raw })
|
|
512
|
+
break
|
|
513
|
+
}
|
|
514
|
+
} catch { /* Invalid quoted key text has no decoded field identity. */ }
|
|
515
|
+
}
|
|
516
|
+
return values
|
|
517
|
+
}
|
|
518
|
+
|
|
408
519
|
function isInitializationDefault(field, value) {
|
|
409
520
|
const defaults = SEMANTIC_INITIALIZATION_DEFAULTS[field]
|
|
410
521
|
return Array.isArray(defaults) && defaults.includes(value)
|
|
411
522
|
}
|
|
412
523
|
|
|
524
|
+
/** Diff sections with their hunks, read by the same parser as the content scan. */
|
|
413
525
|
export function diffFileSections(diff) {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
current = { path: header ? normalize(header[2]) : null, hunks: [], lines: [] }
|
|
421
|
-
files.push(current)
|
|
422
|
-
hunk = null
|
|
423
|
-
continue
|
|
424
|
-
}
|
|
425
|
-
if (!current) continue
|
|
426
|
-
current.lines.push(line)
|
|
427
|
-
if (line.startsWith('@@')) {
|
|
428
|
-
hunk = { added: [], removed: [] }
|
|
429
|
-
current.hunks.push(hunk)
|
|
430
|
-
continue
|
|
431
|
-
}
|
|
432
|
-
if (!hunk) continue
|
|
433
|
-
if (/^\+(?!\+)/.test(line)) hunk.added.push(line.slice(1))
|
|
434
|
-
else if (/^-(?!-)/.test(line)) hunk.removed.push(line.slice(1))
|
|
435
|
-
}
|
|
436
|
-
return files
|
|
526
|
+
return parseDiffSections(diff, { detail: true }).map((section) => ({
|
|
527
|
+
path: section.path,
|
|
528
|
+
hunks: section.hunks,
|
|
529
|
+
lines: section.lines,
|
|
530
|
+
incomplete: section.incomplete,
|
|
531
|
+
}))
|
|
437
532
|
}
|
|
438
533
|
|
|
439
534
|
export function semanticChangesInFile(file) {
|
|
440
535
|
const changes = []
|
|
441
536
|
for (const hunk of file.hunks ?? []) {
|
|
442
|
-
const pending = hunk.removed.
|
|
443
|
-
for (const entry of hunk.added.
|
|
537
|
+
const pending = hunk.removed.flatMap(semanticFieldsOnLine)
|
|
538
|
+
for (const entry of hunk.added.flatMap(semanticFieldsOnLine)) {
|
|
444
539
|
const index = pending.findIndex((item) => item.field === entry.field)
|
|
445
540
|
const prior = index === -1 ? null : pending.splice(index, 1)[0]
|
|
446
541
|
if (prior) {
|
|
@@ -456,26 +551,38 @@ export function semanticChangesInFile(file) {
|
|
|
456
551
|
return changes
|
|
457
552
|
}
|
|
458
553
|
|
|
459
|
-
function semanticDiffFindings({
|
|
554
|
+
function semanticDiffFindings({ targets, gitExecutable = 'git' }) {
|
|
460
555
|
const findings = []
|
|
461
556
|
const severity = 'error'
|
|
462
|
-
for (const repo of
|
|
463
|
-
if (!
|
|
557
|
+
for (const [repo, target] of targets) {
|
|
558
|
+
if (!target.root || !fs.existsSync(path.join(target.root, '.git'))) {
|
|
464
559
|
findings.push(unscannableRepoFinding(repo, 'configured path is absent or is not a Git checkout'))
|
|
465
560
|
continue
|
|
466
561
|
}
|
|
467
|
-
|
|
468
|
-
|
|
562
|
+
// --text: a document declared binary, or holding a NUL byte, still has its fields read.
|
|
563
|
+
const result = gitOutputResult(target.root, [...canonicalDiffArgs(['--cached'], ['--text', '--no-renames']), '--', '*.md', '*.kg.json'], { gitExecutable, indexFile: target.indexFile, maxBuffer: CHECK_AGGREGATE_MAX_BYTES })
|
|
564
|
+
if (!result.ok) {
|
|
469
565
|
findings.push(unscannableRepoFinding(repo, 'git diff --cached failed'))
|
|
470
566
|
continue
|
|
471
567
|
}
|
|
472
568
|
if (!result.stdout.trim()) continue
|
|
473
569
|
for (const file of diffFileSections(result.stdout)) {
|
|
570
|
+
if (file.incomplete) {
|
|
571
|
+
findings.push(finding({
|
|
572
|
+
severity,
|
|
573
|
+
code: 'semantic-field-scan-incomplete',
|
|
574
|
+
repo: repo.name,
|
|
575
|
+
path: file.path,
|
|
576
|
+
message: `${repo.name}/${file.path ?? '(unknown path)'}: the staged diff could not be read, so its semantic fields were not checked`,
|
|
577
|
+
details: { reason: file.incomplete },
|
|
578
|
+
}))
|
|
579
|
+
continue
|
|
580
|
+
}
|
|
474
581
|
const changes = semanticChangesInFile(file)
|
|
475
582
|
if (!changes.length) continue
|
|
476
|
-
// The marker has to
|
|
477
|
-
// a marker elsewhere in the commit approves nothing.
|
|
478
|
-
if (
|
|
583
|
+
// The marker has to be added in the same file's diff as the change it approves:
|
|
584
|
+
// a marker elsewhere in the commit, a removed one, or one in a file name approves nothing.
|
|
585
|
+
if (file.hunks.some((hunk) => hunk.added.some((line) => REVIEW_MARKER_RE.test(line)))) continue
|
|
479
586
|
const label = `${repo.name}/${file.path ?? '(unknown path)'}`
|
|
480
587
|
findings.push(
|
|
481
588
|
finding({
|
|
@@ -492,17 +599,17 @@ function semanticDiffFindings({ policy, project, gitExecutable = 'git' }) {
|
|
|
492
599
|
return findings
|
|
493
600
|
}
|
|
494
601
|
|
|
495
|
-
function stagedContentFindings({ policy,
|
|
602
|
+
function stagedContentFindings({ policy, targets, gitExecutable = 'git' }) {
|
|
496
603
|
const rules = resolveContentRules(policy)
|
|
497
604
|
const exceptions = asArray(policy?.contentRuleExceptions)
|
|
498
605
|
const findings = []
|
|
499
606
|
let totalBytes = 0
|
|
500
|
-
for (const repo of
|
|
501
|
-
if (!
|
|
607
|
+
for (const [repo, target] of targets) {
|
|
608
|
+
if (!target.root || !fs.existsSync(path.join(target.root, '.git'))) {
|
|
502
609
|
findings.push(unscannableRepoFinding(repo, 'configured path is absent or is not a Git checkout'))
|
|
503
610
|
continue
|
|
504
611
|
}
|
|
505
|
-
const result = scanStagedRepository({ repoRoot:
|
|
612
|
+
const result = scanStagedRepository({ repoRoot: target.root, rules, exceptions, repo: repo.name, gitExecutable, indexFile: target.indexFile })
|
|
506
613
|
findings.push(...result.findings, ...result.diagnostics)
|
|
507
614
|
totalBytes += result.bytes
|
|
508
615
|
if (totalBytes > CHECK_AGGREGATE_MAX_BYTES) {
|
|
@@ -545,7 +652,7 @@ function repoForCwd(project, cwd) {
|
|
|
545
652
|
* which reports without blocking, so an accepted usage elsewhere in the repo can
|
|
546
653
|
* never strand unrelated work on the machine.
|
|
547
654
|
*/
|
|
548
|
-
export function checkPushContent({ project, policy, repo, updates, cwd = process.cwd(), gitRunner = spawnSync } = {}) {
|
|
655
|
+
export function checkPushContent({ project, policy, repo, updates, remote = null, cwd = process.cwd(), gitRunner = spawnSync } = {}) {
|
|
549
656
|
const target = repo ?? repoForCwd(project, cwd)
|
|
550
657
|
if (!target) {
|
|
551
658
|
// Fail closed. This hook is only installed into managed repos, so failing to
|
|
@@ -563,15 +670,18 @@ export function checkPushContent({ project, policy, repo, updates, cwd = process
|
|
|
563
670
|
const findings = []
|
|
564
671
|
let totalBytes = 0
|
|
565
672
|
for (const update of updates) {
|
|
566
|
-
const result = scanPushUpdate({ repoRoot: target.path, update, rules, exceptions, repo: target.name, gitRunner })
|
|
673
|
+
const result = scanPushUpdate({ repoRoot: target.path, update, rules, exceptions, repo: target.name, remote, gitRunner, maxBytes: CHECK_AGGREGATE_MAX_BYTES - totalBytes, aggregateLimitBytes: CHECK_AGGREGATE_MAX_BYTES })
|
|
567
674
|
findings.push(...result.findings, ...result.diagnostics)
|
|
568
675
|
totalBytes += result.bytes
|
|
676
|
+
// The per-commit scan reports its own budget refusal; say it once.
|
|
677
|
+
if (result.diagnostics.some((item) => item.details?.reason === 'push-evidence-too-large')) break
|
|
569
678
|
if (totalBytes > CHECK_AGGREGATE_MAX_BYTES) {
|
|
570
679
|
findings.push(finding({
|
|
571
680
|
severity: 'error',
|
|
572
681
|
code: 'content-scan-incomplete',
|
|
573
682
|
repo: target.name,
|
|
574
|
-
message: `push evidence exceeds the ${CHECK_AGGREGATE_MAX_BYTES}-byte aggregate limit`,
|
|
683
|
+
message: `push evidence exceeds the ${CHECK_AGGREGATE_MAX_BYTES}-byte aggregate limit; push it in smaller parts`,
|
|
684
|
+
details: { reason: 'push-evidence-too-large', limitBytes: CHECK_AGGREGATE_MAX_BYTES },
|
|
575
685
|
}))
|
|
576
686
|
break
|
|
577
687
|
}
|
|
@@ -665,7 +775,7 @@ function promotionFindings({ policy, project, graph }) {
|
|
|
665
775
|
return findings
|
|
666
776
|
}
|
|
667
777
|
|
|
668
|
-
export function checkBoundaryPolicy({ project, policy, staged = false, stagedOnly = false, actor = null, gitExecutable = 'git', allowNetworkActorResolution = true, allowHistoryActorResolution = false, forceActorErrors = false, delegations = null, ownerKeys = null, now = Date.now() } = {}) {
|
|
778
|
+
export function checkBoundaryPolicy({ project, policy, staged = false, stagedOnly = false, actor = null, gitExecutable = 'git', allowNetworkActorResolution = true, allowHistoryActorResolution = false, forceActorErrors = false, delegations = null, ownerKeys = null, now = Date.now(), hookIndex = null } = {}) {
|
|
669
779
|
const validationErrors = validateBoundaryPolicy(policy, project)
|
|
670
780
|
let graph = null
|
|
671
781
|
const findings = validationErrors.map((message) => finding({ severity: 'error', code: 'boundary-policy-invalid', message }))
|
|
@@ -684,10 +794,18 @@ export function checkBoundaryPolicy({ project, policy, staged = false, stagedOnl
|
|
|
684
794
|
const operation = staged ? 'pre-commit' : 'boundary-check'
|
|
685
795
|
findings.push(...actorFindings({ policy, project, actor, gitExecutable, allowNetworkActorResolution, allowHistoryActorResolution, forceActorErrors, operation, delegations: delegationList, ownerKeys, now }))
|
|
686
796
|
if (staged) {
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
797
|
+
const { targets, unresolved } = stagedTargets(project, hookIndex, gitExecutable)
|
|
798
|
+
if (unresolved) {
|
|
799
|
+
findings.push(finding({
|
|
800
|
+
code: 'pre-commit-repo-unresolved',
|
|
801
|
+
message: `${unresolved} is not a repo declared in ${project?.configPath ?? 'the project config'}, or several declared repos share its Git directory; the pre-commit guard cannot tell what is being committed`,
|
|
802
|
+
}))
|
|
803
|
+
}
|
|
804
|
+
const listed = stagedPathsResult(targets, { gitExecutable })
|
|
805
|
+
findings.push(...listed.failures.map(({ repo, reason }) => unscannableRepoFinding(repo, `staged paths could not be listed (${reason})`)))
|
|
806
|
+
findings.push(...forbiddenPathFindings({ policy, stagedPaths: listed.paths }))
|
|
807
|
+
findings.push(...semanticDiffFindings({ targets, gitExecutable }))
|
|
808
|
+
findings.push(...stagedContentFindings({ policy, targets, gitExecutable }))
|
|
691
809
|
}
|
|
692
810
|
const errors = findings.filter((item) => item.severity === 'error')
|
|
693
811
|
const warnings = findings.filter((item) => item.severity !== 'error')
|
|
@@ -715,7 +833,7 @@ export function runBoundaryCheckCommand(argv = process.argv.slice(2)) {
|
|
|
715
833
|
// Owner keys are read only when a delegations document exists.
|
|
716
834
|
const ownerKeys = sidecar.present && sidecar.document ? ownerKeysForCommand() : null
|
|
717
835
|
let report = loaded.ok
|
|
718
|
-
? checkBoundaryPolicy({ project, policy: loaded.policy, staged, stagedOnly, actor: firstString(args.actor), delegations: sidecar.document, ownerKeys })
|
|
836
|
+
? checkBoundaryPolicy({ project, policy: loaded.policy, staged, stagedOnly, actor: firstString(args.actor), delegations: sidecar.document, ownerKeys, hookIndex: staged ? hookIndexFromEnvironment() : null })
|
|
719
837
|
: { ok: false, mode: null, schema: BOUNDARY_POLICY_SCHEMA, graphCounts: null, findings: missingFindings, errors: missingFindings, warnings: [] }
|
|
720
838
|
if (sidecarFindings.length) report = { ...report, ok: false, findings: [...sidecarFindings, ...report.findings], errors: [...sidecarFindings, ...report.errors] }
|
|
721
839
|
|
|
@@ -724,9 +842,12 @@ export function runBoundaryCheckCommand(argv = process.argv.slice(2)) {
|
|
|
724
842
|
} else {
|
|
725
843
|
const label = report.ok ? 'passed' : 'failed'
|
|
726
844
|
console.log(`[boundary:check] ${label} · ${report.errors.length ?? report.findings.filter((item) => item.severity === 'error').length} errors · ${report.warnings.length ?? report.findings.filter((item) => item.severity !== 'error').length} warnings`)
|
|
727
|
-
|
|
845
|
+
const { shown, hidden } = printedFindings(report.findings)
|
|
846
|
+
for (const item of shown) console.log(`${item.severity ?? 'error'} ${item.code}: ${item.message}`)
|
|
847
|
+
if (hidden) console.log(`${hidden} more not shown; --json lists every finding`)
|
|
728
848
|
}
|
|
729
|
-
process
|
|
849
|
+
// Let queued stdout drain before the command process exits, including a complete JSON report through a pipe.
|
|
850
|
+
process.exitCode = report.ok ? 0 : 1
|
|
730
851
|
}
|
|
731
852
|
|
|
732
853
|
export function installBoundaryHooks({ project, force = false } = {}) {
|
|
@@ -784,7 +905,7 @@ ATELIER_HOOK_PROJECT_CONFIG="$ATELIER_HOOK_REPO_ROOT"/${shellSingleQuote(portabl
|
|
|
784
905
|
// pre-push reads the ref updates git writes to stdin and judges only that range;
|
|
785
906
|
// pre-commit judges the staged diff. Neither scans the whole tree — that view is
|
|
786
907
|
// `atelier boundary audit`, which reports without blocking.
|
|
787
|
-
const invocation = hookName === 'pre-push' ? `boundary push-check ${config}` : `boundary check --staged ${config}`
|
|
908
|
+
const invocation = hookName === 'pre-push' ? `boundary push-check ${config} --remote="\${1:-}"` : `boundary check --staged ${config}`
|
|
788
909
|
return `#!/usr/bin/env bash
|
|
789
910
|
# MNSTRY_ATELIER_BOUNDARY_GUARD ${hookName}
|
|
790
911
|
set -euo pipefail
|
|
@@ -830,13 +951,19 @@ export function runBoundaryPushCheckCommand(argv = process.argv.slice(2), { stdi
|
|
|
830
951
|
console.log('[boundary:push-check] no ref updates on stdin · nothing to judge')
|
|
831
952
|
process.exit(0)
|
|
832
953
|
}
|
|
833
|
-
|
|
954
|
+
// The installed hook passes the remote Git names as its first argument.
|
|
955
|
+
const report = checkPushContent({ project, policy: loaded.policy, updates: parsed.updates, remote: firstString(args.remote), cwd: process.cwd() })
|
|
834
956
|
if (args.json) {
|
|
835
957
|
console.log(JSON.stringify(report, null, 2))
|
|
836
958
|
} else {
|
|
837
959
|
console.log(`[boundary:push-check] ${report.ok ? 'passed' : 'blocked'} · ${report.repo ?? 'unknown repo'} · ${report.errors.length} errors · ${report.warnings.length} warnings`)
|
|
838
|
-
|
|
839
|
-
|
|
960
|
+
const { shown, hidden } = printedFindings(report.findings)
|
|
961
|
+
for (const item of shown) {
|
|
962
|
+
const also = item.commits?.length ? ` (also in commits ${item.commits.map((commit) => commit.slice(0, 12)).join(', ')})` : ''
|
|
963
|
+
console.log(`${item.severity} ${item.rule ?? item.code}: ${item.message}${also}`)
|
|
964
|
+
}
|
|
965
|
+
if (hidden) console.log(`${hidden} more not shown; --json lists every finding`)
|
|
966
|
+
if (!report.ok && report.repo && report.errors.some((item) => item.code === 'content-rule-violation')) {
|
|
840
967
|
console.log('')
|
|
841
968
|
console.log('If this usage is intentional, declare it in the boundary policy:')
|
|
842
969
|
console.log(' contentRuleExceptions: [{ rule, repo, paths: [...], reason: "why this is accepted" }]')
|
package/src/cli/run.mjs
CHANGED
|
@@ -48,6 +48,11 @@ export const commandMap = new Map([
|
|
|
48
48
|
['config', ['src/commands/config.mjs']],
|
|
49
49
|
['config:check', ['src/commands/config.mjs']],
|
|
50
50
|
['manifest', ['src/commands/config.mjs']],
|
|
51
|
+
['extension', ['src/commands/extension.mjs']],
|
|
52
|
+
['extension:init', ['src/commands/extension.mjs', 'init']],
|
|
53
|
+
['extension:validate', ['src/commands/extension.mjs', 'validate']],
|
|
54
|
+
['extension:seal', ['src/commands/extension.mjs', 'seal']],
|
|
55
|
+
['extension:bundle', ['src/commands/extension.mjs', 'bundle']],
|
|
51
56
|
['extension-pack', ['src/commands/extension-pack.mjs']],
|
|
52
57
|
['extension-pack:validate', ['src/commands/extension-pack.mjs', 'validate']],
|
|
53
58
|
['extension-pack:list', ['src/commands/extension-pack.mjs', 'list']],
|
|
@@ -101,7 +106,7 @@ export function buildHelpText(brand = DEFAULT_BRAND, atelierVersion = null) {
|
|
|
101
106
|
|
|
102
107
|
Usage:
|
|
103
108
|
atelier <command> [args]
|
|
104
|
-
mnstry-atelier <command> [args] #
|
|
109
|
+
mnstry-atelier <command> [args] # supported npm alias
|
|
105
110
|
mnstry atelier <command> [args] # compatibility`
|
|
106
111
|
: `${brand.displayName}
|
|
107
112
|
powered by MNSTRY Atelier ${atelierVersion}
|
|
@@ -150,6 +155,7 @@ Core commands:
|
|
|
150
155
|
upgrade explain --plan Explain saved changes and consent without writes.
|
|
151
156
|
upgrade apply --plan --confirm Apply the selected plan through commit hooks.
|
|
152
157
|
upgrade status|recover Inspect receipts or preview recovery.
|
|
158
|
+
upgrade closure --npm-root Plan a consumer dependency-closure check; reads only, proves nothing.
|
|
153
159
|
upgrade --dry-run Plan a legacy package/template upgrade.
|
|
154
160
|
upgrade --apply Apply a branch-based reviewable upgrade.
|
|
155
161
|
lock check|write Verify or create atelier.lock.json.
|
|
@@ -167,6 +173,10 @@ Core commands:
|
|
|
167
173
|
learn capture|propose|decide|activate|withdraw Manage scoped lessons and their evidence.
|
|
168
174
|
ingest plan|run|status|query Preserve selected sources and search bounded evidence.
|
|
169
175
|
config check Validate project config.
|
|
176
|
+
extension init Scaffold a custom database extension.
|
|
177
|
+
extension validate Check extension source and release digests.
|
|
178
|
+
extension seal Seal reviewed extension edits.
|
|
179
|
+
extension bundle Export a sealed host-registration bundle.
|
|
170
180
|
extension-pack validate Validate declared extension packs.
|
|
171
181
|
extension-pack list List declared extension packs.
|
|
172
182
|
distribution check Check a distribution for MNSTRY attribution.
|
|
@@ -192,7 +202,7 @@ export function buildCommandHelpText(command, brand = DEFAULT_BRAND) {
|
|
|
192
202
|
const c = brand.command
|
|
193
203
|
const serverHelp = (name) => `Usage: ${c} ${name} [--project ./atelier.project.json] [--port=PORT] [--review] [--knowledge]
|
|
194
204
|
|
|
195
|
-
Serves the project's built projection on 127.0.0.1 in the foreground until stopped. It builds nothing: run ${c} graph, then ${c} build, with the same --project path first. The port is --port=PORT, else the PORT environment variable, else 8137; 0 picks a free port, and the address printed on start names it. --review also serves the local review workspace at /review (docs/local-review.md). --knowledge adds guided knowledge dashboards and private coauthoring at /knowledge (docs/knowledge-workspace.md).`
|
|
205
|
+
Serves the project's built projection on 127.0.0.1 in the foreground until stopped. It builds nothing: run ${c} graph, then ${c} build, with the same --project path first. The port is --port=PORT, else the PORT environment variable, else 8137; 0 picks a free port, and the address printed on start names it. --review also serves the local review workspace at /review (docs/guides/local-review.md). --knowledge adds guided knowledge dashboards and private coauthoring at /knowledge (docs/guides/knowledge-workspace.md).`
|
|
196
206
|
const help = {
|
|
197
207
|
capability: `Usage: ${c} capability seal|verify|inventory|plan|apply|status|observe|candidates|recover|fleet|graph|profiles
|
|
198
208
|
Run ${c} capability help for options. Releases stay local; adoption is per repository
|
|
@@ -200,10 +210,10 @@ and requires an exact plan digest. Installed bytes, host loading and successful
|
|
|
200
210
|
exercise are separate evidence. No model execution or tool permission grants.`,
|
|
201
211
|
ingest: `Usage: ${c} ingest plan|run|status|query
|
|
202
212
|
|
|
203
|
-
Read one bounded UTF-8 JSON object {workspaceId,input} from stdin inside the intended Git workspace. Plans bind explicit sources, scope and budgets. Run performs bounded local text, CSV and JSON extraction. Query returns source-bound lexical evidence with partial coverage; it does not synthesize or accept claims. Private state stays in ignored .atelier-local/. See docs/ingestion.md.`,
|
|
213
|
+
Read one bounded UTF-8 JSON object {workspaceId,input} from stdin inside the intended Git workspace. Plans bind explicit sources, scope and budgets. Run performs bounded local text, CSV and JSON extraction. Query returns source-bound lexical evidence with partial coverage; it does not synthesize or accept claims. Private state stays in ignored .atelier-local/. See docs/guides/ingestion.md.`,
|
|
204
214
|
learn: `Usage: ${c} learn capture|propose|decide|activate|withdraw|list|context|render|graph|export|plan
|
|
205
215
|
|
|
206
|
-
Read a bounded JSON object from stdin in the intended Git workspace. Every request includes workspaceId. Writes include actor, requestId, expectedRevision and input. Context and render include query. Local actor identity is asserted, not authenticated. State stays in ignored .atelier-local/. Accepted content becomes context only after explicit activation for a named harness; no permission grant, file installation or sending occurs. See docs/learning.md.`,
|
|
216
|
+
Read a bounded JSON object from stdin in the intended Git workspace. Every request includes workspaceId. Writes include actor, requestId, expectedRevision and input. Context and render include query. Local actor identity is asserted, not authenticated. State stays in ignored .atelier-local/. Accepted content becomes context only after explicit activation for a named harness; no permission grant, file installation or sending occurs. See docs/guides/learning.md.`,
|
|
207
217
|
dev: serverHelp('dev'),
|
|
208
218
|
server: serverHelp('server'),
|
|
209
219
|
skills: `Usage: ${c} skills audit|observe|candidates|sync
|
|
@@ -222,7 +232,7 @@ check reports ontology-to-question coverage against the current canonical graph.
|
|
|
222
232
|
context --question TEXT [--mode graph|lexical] [--max-bytes N] prints a bounded JSON packet of complete active Markdown sources.
|
|
223
233
|
evaluate compares lexical and graph selection on the plan's pinned evidence cases; exit 1 means missing or stale expected evidence, missing relationships, or an expected abstention that retrieved or omitted matching evidence.
|
|
224
234
|
The packet is local operator context, may contain private material, and grants no sharing or execution authority. Payload bytes are exact; token estimates are not measured usage. Session operations read JSON on stdin (except list); saved answers are private drafts.
|
|
225
|
-
semantic reads one JSON object {workspaceId, run, request} on stdin (at most 256 KiB) in the intended Git workspace, the one ingest uses. Each operation accepts exactly its documented request fields: required fields non-null, every field of its declared kind; anything else refuses as SEMANTIC_OPERATION_INVALID before anything is written. Formats such as timestamps are the runner's to check. Atelier calls no model: the host supplies its raw output, candidates and usage (null when unknown), and every interpretation stays pending until receiver records accept it; those records are caller assertions, not authenticated identity. Failures print {ok:false, code, error}, with recorded or captured details for reopening when the runner attached them. See docs/knowledge-setup.md, docs/knowledge-workspace.md and docs/semantic-operation.md.`,
|
|
235
|
+
semantic reads one JSON object {workspaceId, run, request} on stdin (at most 256 KiB) in the intended Git workspace, the one ingest uses. Each operation accepts exactly its documented request fields: required fields non-null, every field of its declared kind; anything else refuses as SEMANTIC_OPERATION_INVALID before anything is written. Formats such as timestamps are the runner's to check. Atelier calls no model: the host supplies its raw output, candidates and usage (null when unknown), and every interpretation stays pending until receiver records accept it; those records are caller assertions, not authenticated identity. Failures print {ok:false, code, error}, with recorded or captured details for reopening when the runner attached them. See docs/start/knowledge-setup.md, docs/guides/knowledge-workspace.md and docs/reference/semantic-operation.md.`,
|
|
226
236
|
init: `Usage: ${c} init [--template private-domain|shared-project|sample-workspace|distribution|external-project|knowledge-workspace] [--target DIR] [--actor ID]
|
|
227
237
|
|
|
228
238
|
Creates tracked starter files and an Atelier lockfile. It does not install hooks unless asked separately. An unrecognized --template exits 1 and writes nothing; omit --template for the blank scaffold.`,
|
|
@@ -253,6 +263,13 @@ Writes a minimal <file>.kg.json sidecar next to every .html, .pdf and .docx docu
|
|
|
253
263
|
${c} readiness export --dry-run [--project ./atelier.project.json]
|
|
254
264
|
|
|
255
265
|
Runs the bundled MNSTRY readiness pack claim-first. Protocol state and packet drafts stay in ignored .atelier-local/readiness/ unless explicitly materialized by a reviewed future flow.`,
|
|
266
|
+
extension: `Usage:
|
|
267
|
+
${c} extension init --directory NEW_PATH [--id sample.counter] [--profile postgres-sql|drizzle-db-first|drizzle-code-first|prisma-7-postgres] [--json]
|
|
268
|
+
${c} extension validate --directory PATH [--json]
|
|
269
|
+
${c} extension seal --directory PATH [--json]
|
|
270
|
+
${c} extension bundle --directory PATH [--json]
|
|
271
|
+
|
|
272
|
+
Generate a customer-owned Postgres project or validate/seal its source digests. Init requires a new directory. These commands create no account, database, process, deployment or installation. See docs/guides/extensions.md.`,
|
|
256
273
|
'extension-pack': `Usage:
|
|
257
274
|
${c} extension-pack validate [--json] [--project ./atelier.project.json]
|
|
258
275
|
${c} extension-pack list [--json] [--project ./atelier.project.json]
|
|
@@ -260,7 +277,7 @@ Runs the bundled MNSTRY readiness pack claim-first. Protocol state and packet dr
|
|
|
260
277
|
Loads every extension pack declared under ext["mnstry.atelier"].extensionPacks in the tracked project config and reports one line per pack. Packs load additively alongside the bundled MNSTRY protocols and can never replace them. list is the default subcommand. Shared --project-config and repeated --repo-path NAME=PATH options are supported in both space and equals forms.`,
|
|
261
278
|
distribution: `Usage: ${c} distribution check [--target DIR] [--pack DIR]
|
|
262
279
|
|
|
263
|
-
Checks a distribution package for the required MNSTRY attribution markers. Blocking: the distribution README.md byte check, and a CLI probe that EXECUTES the target's declared bin with --version (spawned with the current Node, cwd set to the target — only run this against distributions you trust) and requires the attribution in its output; a target that looks like a distribution but declares no probe-able bin, or ships a malformed package.json, is also blocking. The extension-pack manifest attribution key is advisory and reported only. The normative wording lives in TRADEMARKS.md under "Required attribution"; see also docs/attestation.md and docs/distributions.md.`,
|
|
280
|
+
Checks a distribution package for the required MNSTRY attribution markers. Blocking: the distribution README.md byte check, and a CLI probe that EXECUTES the target's declared bin with --version (spawned with the current Node, cwd set to the target — only run this against distributions you trust) and requires the attribution in its output; a target that looks like a distribution but declares no probe-able bin, or ships a malformed package.json, is also blocking. The extension-pack manifest attribution key is advisory and reported only. The normative wording lives in TRADEMARKS.md under "Required attribution"; see also docs/reference/attestation.md and docs/guides/distributions.md.`,
|
|
264
281
|
disclosure: `Usage: ${c} disclosure check [--root DIR] [--staged] [--denylist FILE | --structural-only] [--fail-on-binary] [--untrusted]
|
|
265
282
|
|
|
266
283
|
Scans Git-tracked files, or staged index blobs with --staged, without following symlinks. A private denylist is required by default and must be supplied through ATELIER_DENYLIST_JSON, --denylist, or ignored .atelier-local/disclosure-denylist.json. --structural-only is the explicit no-denylist lane. --untrusted suppresses finding details.`,
|
|
@@ -279,7 +296,7 @@ Assembles a local feedback report under ignored .atelier-local/feedback/ (mode 0
|
|
|
279
296
|
announcements: `Usage: ${c} announcements list [--dir DIR] [--public-key FILE] | verify <file> [--public-key FILE] [--json] | show <file> [--public-key FILE]
|
|
280
297
|
|
|
281
298
|
MNSTRY announcements are a pull-only channel: signed JSON documents under announcements/ in the repository. The trust anchor is always the committed MNSTRY key, or one you pass explicitly with --public-key; --dir changes only where documents are read from and never which key verifies them. Every run names the key and keyId it used. The kit never fetches anything — receiving announcements is the git pull you chose to run, and show refuses to print a body whose signature does not verify.`,
|
|
282
|
-
obsidian: `Usage: ${c} obsidian status|settings|location|scope|view|audience|mode|policy|service|open|uninstall|plugin|apply|conflicts|apply-policy|selection|proposals [--project atelier.project.json] [--data-root DIR] [--json] [--no-input]
|
|
299
|
+
obsidian: `Usage: ${c} obsidian status|settings|location|scope|view|audience|mode|policy|service|open|uninstall|plugin|apply|conflicts|apply-policy|selection|proposals [--project atelier.project.json | --personal-home DIR] [--data-root DIR] [--json] [--no-input]
|
|
283
300
|
|
|
284
301
|
${c} obsidian status
|
|
285
302
|
${c} obsidian settings
|
|
@@ -300,7 +317,7 @@ MNSTRY announcements are a pull-only channel: signed JSON documents under announ
|
|
|
300
317
|
${c} obsidian selection resolve ID | persist ID [allow-empty] | show ID | list
|
|
301
318
|
${c} obsidian proposals list | show OPERATION
|
|
302
319
|
|
|
303
|
-
Every input is an argument. view add changes the project configuration, which you commit: it shows the change and how many notes the view would show, and asks a person at a terminal first (--yes answers beforehand); anyone else's command is their consent, and nothing is ever committed. With --json exactly one JSON document is printed, for a refusal too. status, settings, location show, scope, audience show, mode show, policy show, policy digest, plugin show and service status are read-only. open reports current only when the owned maintenance service ticked after the request, the view's trusted generation is the prepared one and reads back byte for byte, and the installed Obsidian meets the minimum version and answers for that vault; every other outcome is typed and exits 3. Reaching the installed app is never a default: open, service start and service unit need --adapter=obsidian-cli once for a workspace, which remembers it (never used under the test runner). service unit --install installs a login item on macOS (a launchd agent) or Linux (a systemd user unit) so maintenance starts when you log in, under a consent that covers startup; it runs the package installed in the project and is refused from a package runner's cache. Once installed, the service is started through it, and service start replaces a service of an earlier release. service unit --remove removes it and starts the service for this session only. uninstall removes the login item and stops the service, and keeps vaults, private state, the project file and Obsidian's vault list. Obsidian's own settings file is written only while no Obsidian runs, and every write is shown in open's answer: the view's vault is added to its list with a backup of the file as it was, its command line is turned on (cli: true), and for an Obsidian that never started the file is created. A running Obsidian that open cannot reach (its command line turned off, or no vault open) is quit and started again only with --restart-obsidian, which no other flag implies; otherwise open names that flag. --wait-ms bounds open's wait for maintenance's tick. The first start of the maintenance service records who allowed it: --consent-actor ID, or for a person at a terminal the account's name; --json, --no-input, CI or ATELIER_NONINTERACTIVE=1 mean nobody is at a terminal. Audiences (audience set me: only you, with the notes that carry no classification, which no other list shows), mode, the apply policy and what was decided are private machine settings, never project configuration and never a note. apply, conflicts, apply-policy, selection and proposals are contributed operations registered from modules shipped under src/runtime/obsidian/contributions/; the built-in names cannot be replaced. While no apply operation is registered every apply reports apply-unavailable. Exit codes: 0 done, 1 internal error, 2 refusal or usage, 3 ran and the answer is not success.`,
|
|
320
|
+
Every input is an argument. view add changes the project configuration, which you commit: it shows the change and how many notes the view would show, and asks a person at a terminal first (--yes answers beforehand); anyone else's command is their consent, and nothing is ever committed. With --json exactly one JSON document is printed, for a refusal too. status, settings, location show, scope, audience show, mode show, policy show, policy digest, plugin show and service status are read-only. open reports current only when the owned maintenance service ticked after the request, the view's trusted generation is the prepared one and reads back byte for byte, and the installed Obsidian meets the minimum version and answers for that vault; every other outcome is typed and exits 3. Reaching the installed app is never a default: open, service start and service unit need --adapter=obsidian-cli once for a workspace, which remembers it (never used under the test runner). service unit --install installs a login item on macOS (a launchd agent) or Linux (a systemd user unit) so maintenance starts when you log in, under a consent that covers startup; it runs the package installed in the project and is refused from a package runner's cache. Once installed, the service is started through it, and service start replaces a service of an earlier release. service unit --remove removes it and starts the service for this session only. uninstall removes the login item and stops the service, and keeps vaults, private state, the project file and Obsidian's vault list. Obsidian's own settings file is written only while no Obsidian runs, and every write is shown in open's answer: the view's vault is added to its list with a backup of the file as it was, its command line is turned on (cli: true), and for an Obsidian that never started the file is created. A running Obsidian that open cannot reach (its command line turned off, or no vault open) is quit and started again only with --restart-obsidian, which no other flag implies; otherwise open names that flag. --wait-ms bounds open's wait for maintenance's tick. The first start of the maintenance service records who allowed it: --consent-actor ID, or for a person at a terminal the account's name; --json, --no-input, CI or ATELIER_NONINTERACTIVE=1 mean nobody is at a terminal. Audiences (audience set me: only you, with the notes that carry no classification, which no other list shows), mode, the apply policy and what was decided are private machine settings, never project configuration and never a note. apply, conflicts, apply-policy, selection and proposals are contributed operations registered from modules shipped under src/runtime/obsidian/contributions/; the built-in names cannot be replaced. While no apply operation is registered every apply reports apply-unavailable. --personal-home DIR, given instead of --project, follows the generation last confirmed in that private home of a personal workspace; nothing here confirms one, and while none is confirmed and current nothing is published and status, open and service start refuse with the reason. Who may see its vaults stays this machine's audience setting: owner only when undecided; an explicit clear stays empty. Exit codes: 0 done, 1 internal error, 2 refusal or usage, 3 ran and the answer is not success.`,
|
|
304
321
|
sync: `Usage:
|
|
305
322
|
${c} sync enroll --repo DIR [--project atelier.project.json] [--git ABSOLUTE_PATH]
|
|
306
323
|
${c} sync status --repo DIR
|
|
@@ -372,7 +389,7 @@ export async function runCli({
|
|
|
372
389
|
|
|
373
390
|
const [command, ...rest] = normalized.args
|
|
374
391
|
if (rest[0] === '--help' || rest[0] === '-h') {
|
|
375
|
-
stdout(buildCommandHelpText(command, brand))
|
|
392
|
+
stdout(buildCommandHelpText(command.startsWith('extension:') ? 'extension' : command, brand))
|
|
376
393
|
return 0
|
|
377
394
|
}
|
|
378
395
|
const target = commandMap.get(command)
|
|
@@ -15,7 +15,7 @@ const ATTRIBUTION_BYTES = 'powered by MNSTRY Atelier'
|
|
|
15
15
|
const ATTRIBUTION_EXT_KEY = 'mnstry.atelier/attribution'
|
|
16
16
|
const PACK_MANIFEST_NAME = 'atelier.pack.json'
|
|
17
17
|
const PREFIX = '[atelier-distribution:check]'
|
|
18
|
-
const POLICY_POINTER = 'see TRADEMARKS.md ("Required attribution") and docs/attestation.md'
|
|
18
|
+
const POLICY_POINTER = 'see TRADEMARKS.md ("Required attribution") and docs/reference/attestation.md'
|
|
19
19
|
|
|
20
20
|
const log = (message) => console.log(`${PREFIX} ${message}`)
|
|
21
21
|
const logError = (message) => console.error(`${PREFIX} ${message}`)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { scaffoldSqlProject, scaffoldConvexProject, sealProject, validateProject, exportReleaseBundle } from '../extensions/project.mjs'
|
|
3
|
+
|
|
4
|
+
const usage = 'Usage: atelier extension init|validate|seal|bundle --directory PATH [--id sample.counter] [--profile postgres-sql|drizzle-db-first|drizzle-code-first|prisma-7-postgres|convex-native] [--json]'
|
|
5
|
+
const [command, ...args] = process.argv.slice(2)
|
|
6
|
+
try {
|
|
7
|
+
if (command === '--help' || command === '-h' || !command) { console.log(usage) }
|
|
8
|
+
else {
|
|
9
|
+
if (!['init', 'validate', 'seal', 'bundle'].includes(command)) throw new Error('unknown extension command')
|
|
10
|
+
const options = {}
|
|
11
|
+
for (let i = 0; i < args.length; i++) {
|
|
12
|
+
const arg = args[i]
|
|
13
|
+
if (arg === '--json') { if (options.json) throw new Error('duplicate option'); options.json = true; continue }
|
|
14
|
+
if (!['--directory', '--id', '--profile'].includes(arg) || !args[i + 1] || args[i + 1].startsWith('--') || Object.hasOwn(options, arg)) throw new Error('invalid option')
|
|
15
|
+
options[arg] = args[++i]
|
|
16
|
+
}
|
|
17
|
+
if (!options['--directory'] || (command !== 'init' && (options['--id'] || options['--profile']))) throw new Error('invalid options')
|
|
18
|
+
if (options['--profile'] && !['postgres-sql','drizzle-db-first','drizzle-code-first','prisma-7-postgres','convex-native'].includes(options['--profile'])) throw new Error('profile not implemented')
|
|
19
|
+
const result = command === 'init' ? await (options['--profile']==='convex-native'?scaffoldConvexProject:scaffoldSqlProject)({ directory: options['--directory'], extensionId: options['--id'], profile: options['--profile'] }) :
|
|
20
|
+
command === 'bundle' ? await exportReleaseBundle(options['--directory']) :
|
|
21
|
+
command === 'validate' ? await validateProject(options['--directory']) : await sealProject(options['--directory']).then(manifest => ({ releaseDigest: manifest.releaseDigest, qualified: false }))
|
|
22
|
+
console.log(command === 'bundle' || options.json ? JSON.stringify(result) : `${command}: ${result.releaseDigest} (qualification pending)`)
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
// Do not echo a caller path, secret, source file or arbitrary exception text.
|
|
26
|
+
console.error(`Extension command failed. ${usage}`); process.exitCode = 1
|
|
27
|
+
}
|
package/src/commands/ingest.mjs
CHANGED
|
@@ -9,7 +9,7 @@ ignored .atelier-local/ state. Plans bind explicit sources, scope and budgets.
|
|
|
9
9
|
Run processes bounded local text, CSV and JSON. Status retains every selected
|
|
10
10
|
source's disposition. Query returns source-bound lexical evidence with coverage;
|
|
11
11
|
no operation accepts semantic claims, calls a provider or enrolls raw files into
|
|
12
|
-
the graph. Output is private JSON. See docs/ingestion.md.`
|
|
12
|
+
the graph. Output is private JSON. See docs/guides/ingestion.md.`
|
|
13
13
|
|
|
14
14
|
try {
|
|
15
15
|
const command = process.argv[2]
|
package/src/commands/learn.mjs
CHANGED
|
@@ -17,7 +17,7 @@ lesson digest. Activate makes approved content available to a named harness;
|
|
|
17
17
|
it does not install files or grant tool permissions. Withdraw stops future use.
|
|
18
18
|
List, context, render, graph, export and plan print private JSON to stdout.
|
|
19
19
|
Export is an explicit private archive, never a send or publication operation.
|
|
20
|
-
See docs/learning.md for complete examples.`
|
|
20
|
+
See docs/guides/learning.md for complete examples.`
|
|
21
21
|
|
|
22
22
|
function exactObject(value, allowed, required) {
|
|
23
23
|
if (!value || typeof value !== 'object' || Array.isArray(value)
|