From a71d7bd377b2e32db7a197d0bcfddcfd4e087310 Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Fri, 31 Jul 2026 15:17:01 +1000 Subject: [PATCH] buildsuite artifact identity fields default from punkproject.toml (punkshell 0.37.0) G-117/G-123 lineage: project, project_url and packager in emitted punkbin-artifact.toml records (runtime family + library tier, both suites) now default from the enclosing project's punkproject.toml [project] table via a bounded walk-up from the build root (common.punkprojectField) - a copied suite reads its own project's manifest. Chains: project: -Dproject > manifest name > "punkshell" (was hardcoded); project_url: -Dprojecturl > manifest url > "unrecorded"; packager: -Dpackager > PUNKBIN_PACKAGER > manifest packager > git identity > fossil user default (new fossil-only fallback) > "unrecorded". Verified end-to-end: suite_tcl90 kit-family + library emissions and the suite_tcl86 library emission carry the manifest values, -D overrides win, family_check green. Root punkproject.toml gains an active url; dev project.new's seeded manifest now carries commented url/packager placeholders documenting the consumption site (punk::mix::commandset::project 0.4.1). Assisted-by: harness=opencode; primary-model=huggingface/moonshotai/Kimi-K3; api-location=unknown --- CHANGELOG.md | 16 ++++++ punkproject.toml | 7 ++- src/buildsuites/suite_tcl86/README.md | 6 ++- src/buildsuites/suite_tcl86/build86.zig | 26 +++++++--- src/buildsuites/suite_tcl86/build_common.zig | 49 +++++++++++++++++++ src/buildsuites/suite_tcl90/README.md | 15 ++++++ src/buildsuites/suite_tcl90/build905.zig | 30 +++++++++--- src/buildsuites/suite_tcl90/build_common.zig | 47 ++++++++++++++++++ .../mix/commandset/project-999999.0a1.0.tm | 9 ++++ .../mix/commandset/project-buildversion.txt | 3 +- 10 files changed, 191 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9bd4acb..8980c6d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ The latest `## [X.Y.Z]` header must match the `version` field in `punkproject.to Entries are newest-first; one bullet per notable change. See the root `AGENTS.md` "Project Versioning" section for the bump policy. +## [0.37.0] - 2026-07-31 + +- Buildsuite artifact identity fields (`project`, `project_url`, `packager`) + now default from the enclosing project's `punkproject.toml` `[project]` + table (G-117/G-123 lineage): both suite recipes locate the nearest + manifest by a bounded walk-up from the build root, so a copied suite + reads its own project's file. Chains - `project`: `-Dproject` > manifest + `name` > "punkshell" (previously hardcoded); `project_url`: + `-Dprojecturl` > manifest `url` > "unrecorded"; `packager`: `-Dpackager` + > `PUNKBIN_PACKAGER` env > manifest `packager` > git identity > `fossil + user default` (new fossil-only fallback) > "unrecorded". Verified + end-to-end in both suites' emissions. The root `punkproject.toml` gained + an active `url`, and `dev project.new`'s seeded manifest now carries + commented `url`/`packager` placeholders documenting where those fields + are consumed (punk::mix::commandset::project 0.4.1). + ## [0.36.0] - 2026-07-31 - `punk-runtime` fetch reports the artifact's build-origin class after diff --git a/punkproject.toml b/punkproject.toml index bc313a20..e020ff64 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,4 +1,9 @@ [project] name = "punkshell" -version = "0.36.0" +version = "0.37.0" license = "BSD-2-Clause" +url = "https://www.gitea1.intx.com.au/jn/punkshell" +#packager: declared identity for published artifacts (declarative, not proof - +#signing is the verification layer). Resolution: -Dpackager > PUNKBIN_PACKAGER +#env > this field > git identity > fossil user default > unrecorded. +#packager = "Julian Noble " diff --git a/src/buildsuites/suite_tcl86/README.md b/src/buildsuites/suite_tcl86/README.md index 457e6399..1044fc5b 100644 --- a/src/buildsuites/suite_tcl86/README.md +++ b/src/buildsuites/suite_tcl86/README.md @@ -74,7 +74,11 @@ ACTIVE - see `goals/G-100-suite-tcl86-tk-tcllib.md` for the progress ledger. Forked from suite_tcl90's step (see its README "Library artifacts (G-138)" for the full mechanism: generation-tagged names, embed-then-hash, punkzip determinism - byte-identical re-emission verified here too, publish - discipline for the non-bit-reproducible dlls, md5c disposition). Runs under + discipline for the non-bit-reproducible dlls, md5c disposition). The artifact + identity fields (`project`, `project_url`, `packager`) resolve per the chains + documented in suite_tcl90's README ("Runtime kit family" section) - this + suite's emission consumes the same `punkproject.toml` defaults and + `-Dproject`/`-Dprojecturl`/`-Dpackager` options. Runs under the INSTALLED STATIC SHELL (this suite has no kit family): sha1/tcllibc resolve from `/lib`, so emission still proves the built shell executes the very packages being published. Revision `-Dlibrev=N` diff --git a/src/buildsuites/suite_tcl86/build86.zig b/src/buildsuites/suite_tcl86/build86.zig index c34370d6..f93d636c 100644 --- a/src/buildsuites/suite_tcl86/build86.zig +++ b/src/buildsuites/suite_tcl86/build86.zig @@ -879,16 +879,23 @@ pub fn build(b: *std.Build) !void { }); //G-117 schema identity options (introduced here by the library step - //this suite emits no runtime-family artifacts): origin = canonical - //artifact repo the artifacts are BUILT FOR; packager = declared - //identity, resolution chain -Dpackager > env PUNKBIN_PACKAGER > - //building checkout's git identity > "unrecorded". + //artifact repo the artifacts are BUILT FOR; project/projecturl/packager + //default from the enclosing project's punkproject.toml [project] table + //(common.punkprojectField: bounded walk-up from the build root - covers + //the suite source dir and the staged _build recipe dir alike; a copied + //suite reads ITS project's manifest). packager = declared identity, + //resolution chain -Dpackager > env PUNKBIN_PACKAGER > punkproject.toml + //[project] packager > building checkout's git identity > 'fossil user + //default' > "unrecorded". const originurl = b.option([]const u8, "originurl", "canonical artifact repo url the library artifacts are built FOR ('origin' field; default punkbin)") orelse "https://www.gitea1.intx.com.au/jn/punkbin"; - const projecturl = b.option([]const u8, "projecturl", "project url recorded in the artifact records ('project_url' field; default unrecorded)") orelse "unrecorded"; + const project = b.option([]const u8, "project", "project name recorded in the artifact records ('project' field; default: punkproject.toml [project] name, else punkshell)") orelse common.punkprojectField(b, "name") orelse "punkshell"; + const projecturl = b.option([]const u8, "projecturl", "project url recorded in the artifact records ('project_url' field; default: punkproject.toml [project] url, else unrecorded)") orelse common.punkprojectField(b, "url") orelse "unrecorded"; const packager = blk: { - if (b.option([]const u8, "packager", "declared packager identity for the artifact records (-Dpackager > env PUNKBIN_PACKAGER > git identity > unrecorded)")) |p| break :blk p; + if (b.option([]const u8, "packager", "declared packager identity for the artifact records (-Dpackager > env PUNKBIN_PACKAGER > punkproject.toml [project] packager > git identity > fossil user default > unrecorded)")) |p| break :blk p; if (b.graph.environ_map.get("PUNKBIN_PACKAGER")) |p| { if (p.len != 0) break :blk p; } + if (common.punkprojectField(b, "packager")) |p| break :blk p; //out_code is only meaningful on some runAllowFail paths - judge //success by usable trimmed output, not the code var gc: u8 = undefined; @@ -904,6 +911,13 @@ pub fn build(b: *std.Build) !void { break :blk nm; } } + //fossil leg: the fossil-only project fallback - bare default user + //(fossil has no email convention); declarative like the rest. + const fuser: ?[]const u8 = b.runAllowFail(&.{ "fossil", "user", "default" }, &gc, .ignore) catch null; + if (fuser != null) { + const fu = std.mem.trim(u8, fuser.?, " \t\r\n"); + if (fu.len != 0) break :blk b.dupe(fu); + } break :blk "unrecorded"; }; //source checkout provenance: tcllib manifest.uuid (suite.tcl's fossil @@ -929,7 +943,7 @@ pub fn build(b: *std.Build) !void { lib_emit.addFileArg(b.path("tools/library_artifacts.tcl")); lib_emit.addArgs(&.{ "-outdir", libart_outdir, "-rev", b.fmt("{d}", .{librev}), "-generation", "tcl8", "-target", "win32-x86_64" }); lib_emit.addArgs(&.{ "-tclpatch", tcl_h_patchlevel, "-suite", "suite_tcl86", "-zig", builtin.zig_version_string, "-optimize", @tagName(optimize) }); - lib_emit.addArgs(&.{ "-origin", originurl, "-packager", packager, "-project", "punkshell", "-projecturl", projecturl }); + lib_emit.addArgs(&.{ "-origin", originurl, "-packager", packager, "-project", project, "-projecturl", projecturl }); lib_emit.addArgs(&.{ "-provenance", b.fmt("tcllib {s} critcl {s}", .{ uuid_tcllib, critcl_uuid }) }); lib_emit.addArg("-punkzip"); lib_emit.addArtifactArg(punkzip_exe); diff --git a/src/buildsuites/suite_tcl86/build_common.zig b/src/buildsuites/suite_tcl86/build_common.zig index e00e40b5..06d9af14 100644 --- a/src/buildsuites/suite_tcl86/build_common.zig +++ b/src/buildsuites/suite_tcl86/build_common.zig @@ -84,6 +84,55 @@ pub fn manifestUuid(b: *std.Build, tree_from_root: []const u8) []const u8 { return std.mem.trim(u8, data, " \t\r\n"); } +//punkproject.toml [project] field reader (G-117/G-123 lineage; mirror of the +//suite_tcl90 helper - shared scaffolding stays duplicated per this suite's +//fork note): bounded walk-up from the build root for the NEAREST +//punkproject.toml - covers the suite source dir (2 up to the project root) and +//the staged _build//build recipe dir (5 up) alike; _build itself never +//carries one. The nearest file is the project boundary: a missing key in it +//means undeclared (null), never a walk into an outer project's identity. +//Line-based [project]-section scan for a quoted key - the same tolerant +//line-based consumer style as the punkbin record readers ('#' comment lines +//and unknown keys ignored). +pub fn punkprojectField(b: *std.Build, key: []const u8) ?[]const u8 { + var rel: []const u8 = "punkproject.toml"; + var depth: usize = 0; + while (depth < 10) : (depth += 1) { + const abs = b.pathFromRoot(rel); + if (std.Io.Dir.cwd().readFileAlloc(b.graph.io, abs, b.allocator, .limited(64 * 1024)) catch null) |data| { + return punkprojectParse(b, data, key); + } + rel = b.fmt("../{s}", .{rel}); + } + return null; +} + +//line-based [project]-section scan for a quoted key: section headers may carry +//a trailing comment; values are simple quoted strings (urls, names - no escape +//processing); first match wins. +fn punkprojectParse(b: *std.Build, data: []const u8, key: []const u8) ?[]const u8 { + var in_project = false; + var lines = std.mem.splitScalar(u8, data, '\n'); + while (lines.next()) |line| { + const t = std.mem.trim(u8, line, " \t\r"); + if (t.len == 0 or t[0] == '#') continue; + if (t[0] == '[') { + in_project = std.mem.startsWith(u8, t, "[project]") and + (t.len == "[project]".len or t["[project]".len] == ' ' or t["[project]".len] == '\t' or t["[project]".len] == '#'); + continue; + } + if (!in_project) continue; + if (!std.mem.startsWith(u8, t, key)) continue; + const after_key = t[key.len..]; + if (after_key.len == 0 or (after_key[0] != ' ' and after_key[0] != '\t' and after_key[0] != '=')) continue; + const q1 = std.mem.indexOfScalar(u8, after_key, '"') orelse continue; + const rest = after_key[q1 + 1 ..]; + const q2 = std.mem.indexOfScalar(u8, rest, '"') orelse continue; + return b.dupe(rest[0..q2]); + } + return null; +} + //package version from a TEA tree's configure.ac 'AC_INIT([name],[version])' //line (thread, tclvfs, ...). Version-derived artifact names must follow the //checkout - hardcoded versions stamp mismatched sources (suite_tcl90's G-107 diff --git a/src/buildsuites/suite_tcl90/README.md b/src/buildsuites/suite_tcl90/README.md index da9f7ef4..49e82cb1 100644 --- a/src/buildsuites/suite_tcl90/README.md +++ b/src/buildsuites/suite_tcl90/README.md @@ -162,6 +162,21 @@ run re-proves the family runtime executes real tooling self-contained. Publication to the real punkbin repo remains a deliberate user step, deferred per the goal notes until the family shape is accepted. +Artifact identity fields (`project`, `project_url`, `packager` - G-117 lineage) +resolve at recipe configure time, for BOTH the family and library emissions. +The chains (`punkproject.toml` = the enclosing project's manifest, found by a +bounded walk-up from the build root - the suite source dir and the staged +`_build` recipe dir both find the project root's file, and a copied suite reads +ITS project's manifest): + +- `project`: `-Dproject` > punkproject.toml `[project] name` > `"punkshell"` +- `project_url`: `-Dprojecturl` > punkproject.toml `[project] url` > `"unrecorded"` +- `packager`: `-Dpackager` > `PUNKBIN_PACKAGER` env > punkproject.toml + `[project] packager` > building checkout's git identity > `fossil user + default` (bare username - the fossil-only fallback) > `"unrecorded"` + +suite.tcl passes the options through `-zigargs` (e.g `-zigargs {-Dprojecturl=https://...}`). + Consumers: working-name runtimes are copied into `bin/runtime/win32-x86_64/` and referenced by `src/runtime/mapvfs.config` (punk -> punk9wintk905.vfs as `punk9_beta`, punk-bi -> punk9win_for_tkruntime.vfs as `punk9bi_beta`, per the diff --git a/src/buildsuites/suite_tcl90/build905.zig b/src/buildsuites/suite_tcl90/build905.zig index 4da7cfb6..edd8a03a 100644 --- a/src/buildsuites/suite_tcl90/build905.zig +++ b/src/buildsuites/suite_tcl90/build905.zig @@ -2434,16 +2434,23 @@ pub fn build(b: *std.Build) !void { const familyrev = b.option(u32, "familyrev", "assembly revision N for the -r family artifact names (kit-family-artifacts; default 2 - r1 published 2026-07-22)") orelse 2; //G-117 schema identity options (v2 as of G-123). origin = canonical artifact repo the //artifacts are BUILT FOR (not necessarily where they end up hosted - - //mirrors preserve it). packager = declared identity, resolution chain - //-Dpackager > env PUNKBIN_PACKAGER > building checkout's git identity > - //"unrecorded" (declarative, not proof - signing is the verification layer). + //mirrors preserve it). project/projecturl/packager default from the enclosing + //project's punkproject.toml [project] table (common.punkprojectField: bounded + //walk-up from the build root - covers the suite source dir and the staged + //_build recipe dir alike; a copied suite reads ITS project's manifest). + //packager = declared identity, resolution chain -Dpackager > env + //PUNKBIN_PACKAGER > punkproject.toml [project] packager > building checkout's + //git identity > 'fossil user default' > "unrecorded" (declarative, not + //proof - signing is the verification layer). const originurl = b.option([]const u8, "originurl", "canonical artifact repo url the family artifacts are built FOR ('origin' field; default punkbin)") orelse "https://www.gitea1.intx.com.au/jn/punkbin"; - const projecturl = b.option([]const u8, "projecturl", "project url recorded in the artifact records ('project_url' field; default unrecorded)") orelse "unrecorded"; + const project = b.option([]const u8, "project", "project name recorded in the artifact records ('project' field; default: punkproject.toml [project] name, else punkshell)") orelse common.punkprojectField(b, "name") orelse "punkshell"; + const projecturl = b.option([]const u8, "projecturl", "project url recorded in the artifact records ('project_url' field; default: punkproject.toml [project] url, else unrecorded)") orelse common.punkprojectField(b, "url") orelse "unrecorded"; const packager = blk: { - if (b.option([]const u8, "packager", "declared packager identity for the artifact records (-Dpackager > env PUNKBIN_PACKAGER > git identity > unrecorded)")) |p| break :blk p; + if (b.option([]const u8, "packager", "declared packager identity for the artifact records (-Dpackager > env PUNKBIN_PACKAGER > punkproject.toml [project] packager > git identity > fossil user default > unrecorded)")) |p| break :blk p; if (b.graph.environ_map.get("PUNKBIN_PACKAGER")) |p| { if (p.len != 0) break :blk p; } + if (common.punkprojectField(b, "packager")) |p| break :blk p; //out_code is only meaningful on some runAllowFail paths - judge //success by usable trimmed output, not the code var gc: u8 = undefined; @@ -2459,6 +2466,13 @@ pub fn build(b: *std.Build) !void { break :blk nm; } } + //fossil leg: the fossil-only project fallback - bare default user + //(fossil has no email convention); declarative like the rest. + const fuser: ?[]const u8 = b.runAllowFail(&.{ "fossil", "user", "default" }, &gc, .ignore) catch null; + if (fuser != null) { + const fu = std.mem.trim(u8, fuser.?, " \t\r\n"); + if (fu.len != 0) break :blk b.dupe(fu); + } break :blk "unrecorded"; }; @@ -2543,7 +2557,7 @@ pub fn build(b: *std.Build) !void { \\#packager: declared identity, not proof - signing (minisign sidecars) \\#is the verification layer. \\packager = "{s}" - \\project = "punkshell" + \\project = "{s}" \\project_url = "{s}" \\#license: summary for the distributed artifact; component license texts \\#ride inside the attached image (tcl_library/license.terms etc). @@ -2566,7 +2580,7 @@ pub fn build(b: *std.Build) !void { \\optimize = "{s}" \\{s} \\ - , .{ artifact_name, fk.variant, fk.working_name, familyrev, build_id, originurl, packager, projecturl, tcl_h_patchlevel, piperepl_block, batteries, builtin.zig_version_string, @tagName(optimize), prov_lines }); + , .{ artifact_name, fk.variant, fk.working_name, familyrev, build_id, originurl, packager, project, projecturl, tcl_h_patchlevel, piperepl_block, batteries, builtin.zig_version_string, @tagName(optimize), prov_lines }); } var family_tree_base: [family_kits.len]std.Build.LazyPath = undefined; @@ -2712,7 +2726,7 @@ pub fn build(b: *std.Build) !void { lib_emit.addFileArg(b.path("tools/library_artifacts.tcl")); lib_emit.addArgs(&.{ "-outdir", libart_outdir, "-rev", b.fmt("{d}", .{librev}), "-generation", "tcl9", "-target", "win32-x86_64" }); lib_emit.addArgs(&.{ "-tclpatch", tcl_h_patchlevel, "-suite", "suite_tcl90", "-zig", builtin.zig_version_string, "-optimize", @tagName(optimize) }); - lib_emit.addArgs(&.{ "-origin", originurl, "-packager", packager, "-project", "punkshell", "-projecturl", projecturl }); + lib_emit.addArgs(&.{ "-origin", originurl, "-packager", packager, "-project", project, "-projecturl", projecturl }); lib_emit.addArgs(&.{ "-provenance", b.fmt("tcllib {s} critcl {s}", .{ uuid_tcllib, critcl_uuid }) }); lib_emit.addArg("-punkzip"); lib_emit.addArtifactArg(punkzip_exe); diff --git a/src/buildsuites/suite_tcl90/build_common.zig b/src/buildsuites/suite_tcl90/build_common.zig index b7cd6044..9bda307d 100644 --- a/src/buildsuites/suite_tcl90/build_common.zig +++ b/src/buildsuites/suite_tcl90/build_common.zig @@ -63,6 +63,53 @@ pub fn manifestUuid(b: *std.Build, tree_from_root: []const u8) []const u8 { return std.mem.trim(u8, data, " \t\r\n"); } +//punkproject.toml [project] field reader (G-117/G-123 lineage): bounded walk-up +//from the build root for the NEAREST punkproject.toml - covers the suite source +//dir (2 up to the project root) and the staged _build//build recipe dir +//(5 up) alike; _build itself never carries one. The nearest file is the project +//boundary: a missing key in it means undeclared (null), never a walk into an +//outer project's identity. Line-based [project]-section scan for a quoted key - +//the same tolerant line-based consumer style as the punkbin record readers +//('#' comment lines and unknown keys ignored). +pub fn punkprojectField(b: *std.Build, key: []const u8) ?[]const u8 { + var rel: []const u8 = "punkproject.toml"; + var depth: usize = 0; + while (depth < 10) : (depth += 1) { + const abs = b.pathFromRoot(rel); + if (std.Io.Dir.cwd().readFileAlloc(b.graph.io, abs, b.allocator, .limited(64 * 1024)) catch null) |data| { + return punkprojectParse(b, data, key); + } + rel = b.fmt("../{s}", .{rel}); + } + return null; +} + +//line-based [project]-section scan for a quoted key: section headers may carry +//a trailing comment; values are simple quoted strings (urls, names - no escape +//processing); first match wins. +fn punkprojectParse(b: *std.Build, data: []const u8, key: []const u8) ?[]const u8 { + var in_project = false; + var lines = std.mem.splitScalar(u8, data, '\n'); + while (lines.next()) |line| { + const t = std.mem.trim(u8, line, " \t\r"); + if (t.len == 0 or t[0] == '#') continue; + if (t[0] == '[') { + in_project = std.mem.startsWith(u8, t, "[project]") and + (t.len == "[project]".len or t["[project]".len] == ' ' or t["[project]".len] == '\t' or t["[project]".len] == '#'); + continue; + } + if (!in_project) continue; + if (!std.mem.startsWith(u8, t, key)) continue; + const after_key = t[key.len..]; + if (after_key.len == 0 or (after_key[0] != ' ' and after_key[0] != '\t' and after_key[0] != '=')) continue; + const q1 = std.mem.indexOfScalar(u8, after_key, '"') orelse continue; + const rest = after_key[q1 + 1 ..]; + const q2 = std.mem.indexOfScalar(u8, rest, '"') orelse continue; + return b.dupe(rest[0..q2]); + } + return null; +} + //package version from a TEA tree's configure.ac 'AC_INIT([name],[version])' //line (thread, tclvfs, ...). G-107: version-derived artifact names must follow //the checkout - hardcoded versions stamp mismatched sources (the thread recipe diff --git a/src/modules/punk/mix/commandset/project-999999.0a1.0.tm b/src/modules/punk/mix/commandset/project-999999.0a1.0.tm index 3434eafe..5f8da802 100644 --- a/src/modules/punk/mix/commandset/project-999999.0a1.0.tm +++ b/src/modules/punk/mix/commandset/project-999999.0a1.0.tm @@ -529,6 +529,15 @@ namespace eval punk::mix::commandset::project { puts $fd "\[project\]" puts $fd "name = \"$projectname\"" puts $fd "version = \"0.1.0\"" + puts $fd "#url: project home/repository url. Consumed by buildsuite artifact" + puts $fd "#emission (punkbin-artifact.toml 'project_url' field; -Dprojecturl" + puts $fd "#overrides). Uncomment and set before publishing artifacts." + puts $fd "#url = \"https://example.com/$projectname\"" + puts $fd "#packager: declared identity for published artifacts (declarative, not" + puts $fd "#proof - signing is the verification layer). Resolution: -Dpackager >" + puts $fd "#PUNKBIN_PACKAGER env > this field > git identity > fossil user default >" + puts $fd "#unrecorded." + puts $fd "#packager = \"Your Name \"" close $fd puts stdout "seeded punkproject.toml (\[project\] name '$projectname' version 0.1.0)" } diff --git a/src/modules/punk/mix/commandset/project-buildversion.txt b/src/modules/punk/mix/commandset/project-buildversion.txt index 8651ceb8..23c682b0 100644 --- a/src/modules/punk/mix/commandset/project-buildversion.txt +++ b/src/modules/punk/mix/commandset/project-buildversion.txt @@ -1,6 +1,7 @@ -0.4.0 +0.4.1 #First line must be a semantic version number #all other lines are ignored. +#0.4.1 - project.new's seeded punkproject.toml now carries commented-out url/packager placeholder fields with '#' documentation lines naming their consumption site (buildsuite artifact emission: punkbin-artifact.toml project_url/packager fields, G-117/G-123 lineage) - generated projects learn where those fields are used; default behaviour unchanged (both stay undeclared until uncommented) #0.4.0 - G-087 stage 4: project.new copies the layout from punk::mix::commandset::layout::lib::layout_materialize's effective content folder - overlay chains (custom-over-vendor merging, .anti deletion markers, name@base derived layouts) are composed into a disposable staging dir (deleted after use); single-folder chains with no .anti markers keep the direct store-folder path (no behaviour change for punk.project / module-carried layouts). Template scanning uses the new path-based layoutfolder_scan_for_template_files against the effective folder. The src/PROJECT_LAYOUTS_* install exclusion was retired with the marker folder (G-087 stage 5) and the misnamed antipaths variable folded into exclude_paths. #0.4.0 - G-087 stage 5: project.new seeds generated-project version metadata post-materialization (all only-if-missing, never from payload): punkproject.toml ([project] name + version 0.1.0), CHANGELOG.md with a matching ## [0.1.0] header (keeps the injected make.tcl projectversion advisory quiet), and punkorigin.toml - an interim generation-provenance baseline (utc, layout name + resolved store target, punk::mix::templates version, punkshell version when derivable from the generating shell's checkout via punkboot::utils, origin projectroot path hint) for the G-027 pull-update design to adopt. #0.3.1 - fix: project.new validates module names (projectname-derived default and explicit -modules entries, modulename-version forms accepted) before any folder/fossil-repo creation - a dashed project name (e.g infra-man) previously failed late inside module.new after the project tree and fossil repo already existed